Skip to Content
@alza/hateoasCoreGetting started

Hateoas form helpers

Installation

yarn add @alza/hateoas --exact

AppLink is a type-safe string union built from the AppLinkDefinition interface via declaration merging. By default it is an empty union (never), meaning appLink on any action or link accepts any string.

Extend it in your project by augmenting the module in a .d.ts file:

// hateoas.d.ts in your project import "@alza/hateoas/app-links"; declare module "@alza/hateoas/app-links" { export interface AppLinkDefinition { customDashboard: never; loyaltyProgram: never; specialOffer: never; } }

After augmentation AppLink resolves to "customDashboard" | "loyaltyProgram" | "specialOffer" and TypeScript will enforce the allowed values everywhere appLink is used.

HateoasLink, AppAction, and NamedAction all accept an optional TAppLink generic that narrows the appLink field. By default TAppLink is the full AppLink union (any registered app link).

Use the generic when you know exactly which app link a particular API response carries:

import type { AppAction, HateoasLink, NamedAction } from "@alza/hateoas"; const someAction: AppAction; someAction.appLink; // any AppLink (full union) const productAction: AppAction<"productLink">; productAction.appLink; // always 'productLink' // Works the same way for HateoasLink and NamedAction const categoryLink: HateoasLink<"categoryPage">; categoryLink.appLink; // always 'categoryPage'

This is especially useful when typing API response models so consumers get precise autocomplete and exhaustive checks on appLink.

Field map utility (auto-mapping)

@alza/hateoas works with a fieldMap object. It is used to map server field names to your local (client/UI) field names.

In many cases you want a 1:1 mapping where local field names are the same as server field names. For that, use:

  • createFieldMapFromForm(form, options) (named export)
  • Hateoas.createFieldMapFromForm(form, options) (same function on the Hateoas helper)

It builds a HateoasFieldMap from HateoasFormV2.value[] and infers "string"/"integer" field types. Unsupported itemTypes are skipped by default.

Terminology (current names):

  • Raw API form type: HateoasFormV2
  • Field map type: HateoasFieldMap

Example: identity mapping

import { Hateoas } from "@alza/hateoas"; const fieldMap = Hateoas.createFieldMapFromForm(form); const fields = Hateoas.getUiFields(form, fieldMap); // fields.password.validators?.(t) ...

Example: rename fields (server -> local)

import { createFieldMapFromForm } from "@alza/hateoas"; const fieldMap = createFieldMapFromForm(form, { rename: { password: "auth.password", }, }); // Use the local name "auth.password" in your UI / form library.

Example: skip or throw on unsupported itemTypes

import { createFieldMapFromForm } from "@alza/hateoas"; // Default: unsupported fields are skipped const fieldMap1 = createFieldMapFromForm(form); // Throw when encountering unsupported fields const fieldMap2 = createFieldMapFromForm(form, { unsupported: "throw" });

Generic parameters

  • required
    • by field.isRequired (V3 has to be true, V2 cannot be false - implicit true)
    • also marks labelAsRequired (*)
  • disabled
    • by field.isEnabled (V3 has to be true, V2 cannot be false - implicit true)
  • label
    • by field.label
  • hidden
    • by field.isHidden
  • placeholder
    • by field.placeholder
  • description
    • by field.description
  • validationError
    • by field.validationError
    • error returned from API

String

Standard text input (field.itemType === 'string' or field.type === 'string')

  • pattern
    • by field.pattern
    • validates string by provided RegExp pattern
  • email
    • by field.semanticItemType === 'email'
    • uses field.pattern if present, otherwise falls back to local email regex
  • phone
    • by field.semanticItemType === 'phone'
    • uses field.pattern if present, otherwise falls back to local phone regex
  • IBAN
    • by field.semanticItemType === 'iban'
    • uses field.pattern if present, otherwise falls back to local IBAN validation
  • minLength
    • by field.minLength or field.min
    • validates string minimal length (characters)
  • maxLength
    • by field.maxLength or field.max
    • validates string maximal length (characters)

Behaviour can also be modified by field.semanticItemType with values of password, textArea.

Integer

Text input limited to numbers (field.itemType === 'string' or field.itemType ==='decimal')

  • max
    • by field.max
    • validates maximum value
  • min
    • by field.min
    • validates minimum value

Boolean

Checkbox input (field.itemType === 'boolean')

Set

Select input (field.itemType === 'set' or field.type === 'set'). Can be multichoice.

  • minSize
    • by field.minSize
    • validates minimum selected size (e.g. you have to pick atleast 3 options)
  • maxSize
    • by field.maxSize
    • validates maximum selected size (e.g. you can pick up to 3 options)

Example: render set as <select>

SetField is the UI model produced by Hateoas.getUiFields(form, fieldMap).

  • field.options contains available options (with disabled already mapped from isEnabled)
  • field.multiple tells you whether to render multi-select
  • field.value is UI-friendly:
    • single-select: string | ""
    • multi-select: string[]
import { Hateoas } from "@alza/hateoas"; import type { HateoasFieldMap, HateoasFormV2, SetField } from "@alza/hateoas"; const fieldMap = { day: ["day", "set"], } as const satisfies HateoasFieldMap; export function Example({ form }: { form: HateoasFormV2 }) { const fields = Hateoas.getUiFields(form, fieldMap); const day = fields.day as SetField; // Render: // <select multiple={day.multiple} defaultValue={day.value}> // {day.options.map(o => ( // <option key={o.value} value={o.value} disabled={o.disabled}> // {o.label} // </option> // ))} // </select> }

Example: submit set values

Server expects set values as an array.

When mapping local UI values back to server values you can pass:

  • single-select value as string → it is coerced to [string]
  • multi-select value as string[] → stays as-is
import { AlzaHateoasForm, Hateoas } from "@alza/hateoas"; import type { HateoasFieldMap, HateoasFormV2 } from "@alza/hateoas"; const fieldMap = { day: ["day", "set"], } as const satisfies HateoasFieldMap; const hateoasFields = Hateoas.getUiFields(form, fieldMap); // UI values (single-select) const localValues = { day: "nextday" }; const serverValues = Hateoas.mapLocalValuesToHateoas( localValues, fieldMap, hateoasFields, ); const hf = new AlzaHateoasForm(form as HateoasFormV2); hf.changeFormValues(serverValues); const submit = hf.getFormSubmitData(); // submit.data.day === ["nextday"]

Range

Slider number input (field.itemType === 'range or field.type === 'range' or field.itemType === 'integer). Pick number between min-max values. // opravdu integer? legacy bug?

DateTime

Date or DateTime input (field.itemType === 'dateTime' or field.semanticItemType === 'date' or field.semanticItemType === 'dateTime'). field.semanticItemType specifies if its date or dateTime.

  • maxDate
    • by field.max
  • minDate
    • by field.min

BlobAttachment

Attachment upload input (field.itemType === 'blobAttachment' or field.type === 'blobAttachment)

  • uploadUrl
    • by field.uploadUrl
    • url where to upload the files (returns attachment ids that are then used as actual values)
  • minSize
    • by field.min
    • minimum attachment count
  • maxSize
    • by field.maxCount
    • maximum attachment count
  • allowedContentTypes
    • by field.allowedContentTypes
    • validates attachment content types (e.g. jpg, png, pdf…)
  • maxAttachmentSize
    • by field.maxAttachmentSize
    • validates each attachment (file) size in Bytes

SubmitButton

Submit button defined from API (field.itemType === 'submitButton or field.type === 'submitButton)

ObjectArray

Custom object array field (field.itemType === 'objectArray) - usually requires custom implementation. Used to just pass predefined complex data structures generated by API.

Last updated on