Skip to content
AzuAzu

Screen forms

Store settings, a profile, or feature switches need a form, but they aren’t one record of a model. A screen can have a form instead of a component. Azu renders it like a resource form and saves through your validator.

Where the values live is up to you. load reads them, and save writes them.

Terminal window
node ace azu:screen StoreSettings --form

That writes the screen and a validator in app/validators/store_settings.ts. Fill in load and save. The demo keeps its settings in a key and value table, through its own Setting model:

app/admin/screens/store_settings.ts
import { defineScreen, field } from '@mezielabs/azu'
import { DateTime } from 'luxon'
import { manageSettings } from '#abilities/main'
import Setting from '#models/setting'
import { storeSettingsValidator } from '#validators/store_settings'
/**
* The store's settings (FR-23): a form Azu renders, for admins only
* (manageSettings). `load` and `save` go through the app's own Setting
* model; Azu never touches the settings table.
*/
export default defineScreen({
label: 'Store settings',
group: 'Settings',
icon: 'settings',
ability: manageSettings,
form: {
fields: [
field.text('storeName'),
field.text('supportEmail').help('Shown to customers on receipts and the contact page'),
field.select('currency').options({
NGN: 'Nigerian naira',
GHS: 'Ghanaian cedi',
KES: 'Kenyan shilling',
ZAR: 'South African rand',
USD: 'US dollar',
GBP: 'British pound',
}),
field.decimal('vatRate').label('VAT rate (%)').min(0).max(100).step('0.01'),
field.boolean('maintenanceMode').help('Customers see a notice instead of the shop'),
field.richText('checkoutAnnouncement').help('Shown above the basket at checkout'),
field.datetime('lastChangedAt').label('Last changed').readOnly(),
],
validator: storeSettingsValidator,
load: () =>
Setting.values([
'storeName',
'supportEmail',
'currency',
'vatRate',
'maintenanceMode',
'checkoutAnnouncement',
'lastChangedAt',
]),
save: (input) => Setting.store({ ...input, lastChangedAt: DateTime.now() }),
},
})

And its validator:

app/validators/store_settings.ts
import vine from '@vinejs/vine'
import { CURRENCIES } from '#validators/coupon'
import { decimalString, labelled, maxDecimal, twoPlaces } from '#validators/messages'
/**
* The Store settings screen's validator (FR-23). Azu hands it only the
* form's writable fields; "last changed" is set by the screen's `save`.
*/
export const storeSettingsValidator = labelled(
vine.create({
storeName: vine.string().trim().minLength(2).maxLength(80),
supportEmail: vine.string().trim().email().maxLength(254),
currency: vine.enum(CURRENCIES),
// Stored as text, so kept in one format: "7.5" is saved as "7.50"
vatRate: decimalString(0)
.use(maxDecimal({ max: 100 }))
.transform(twoPlaces),
maintenanceMode: vine.boolean(),
checkoutAnnouncement: vine.string().trim().maxLength(2000).nullable().optional(),
}),
{
storeName: 'Store name',
supportEmail: 'Support email',
currency: 'Currency',
vatRate: 'VAT rate',
maintenanceMode: 'Maintenance mode',
checkoutAnnouncement: 'Checkout announcement',
},
{ email: '{{ field }} must be an email address' }
)
  • form or component, not both. No data beside a form either: its values come from load. Either mistake is a type error and stops the app from booting.
  • Field names are checked against what load returns. A misspelled field fails tsc, as on resources.
  • Supported fields: text, textarea, number, decimal, boolean, select, date, datetime, and rich text. File, image, and relation fields aren’t supported on screens yet.
  • The screen’s ability covers the form, for opening it and for saving it. So fields have no .ability() here. .readOnly() works as on resources.

Saving works like a resource form.

  1. Only the form’s writable fields are read from the request.
  2. The validator checks them. Read-only fields reach the validator with load’s values.
  3. save gets the validator’s output and nothing else.

Rich text is sanitised, and datetimes are read in the panel’s timezone.

A valid save comes back to the screen with a toast: “Store settings saved”, or your successMessage. Invalid input shows the errors under each field, with the input kept.

It’s the resource form’s layout: the same widths, required markers, and limits from the validator. It has the sticky save bar with one “Save changes” button, and the “Leave without saving?” question. See Forms.