Hateoas form helpers
Installation
yarn add @alza/hateoas --exactAppLink — extending the app link registry
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, NamedAction — generic app link
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 theHateoashelper)
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 (*)
- by
- disabled
- by
field.isEnabled(V3 has to be true, V2 cannot be false - implicit true)
- by
- label
- by
field.label
- by
- hidden
- by
field.isHidden
- by
- placeholder
- by
field.placeholder
- by
- description
- by
field.description
- by
- validationError
- by
field.validationError - error returned from API
- by
String
Standard text input (field.itemType === 'string' or field.type === 'string')
- pattern
- by
field.pattern - validates string by provided RegExp pattern
- by
- email
- by
field.semanticItemType === 'email' - uses
field.patternif present, otherwise falls back to local email regex
- by
- phone
- by
field.semanticItemType === 'phone' - uses
field.patternif present, otherwise falls back to local phone regex
- by
- IBAN
- by
field.semanticItemType === 'iban' - uses
field.patternif present, otherwise falls back to local IBAN validation
- by
- minLength
- by
field.minLengthorfield.min - validates string minimal length (characters)
- by
- maxLength
- by
field.maxLengthorfield.max - validates string maximal length (characters)
- by
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
- by
- min
- by
field.min - validates minimum value
- by
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)
- by
- maxSize
- by
field.maxSize - validates maximum selected size (e.g. you can pick up to 3 options)
- by
Example: render set as <select>
SetField is the UI model produced by Hateoas.getUiFields(form, fieldMap).
field.optionscontains available options (withdisabledalready mapped fromisEnabled)field.multipletells you whether to render multi-selectfield.valueis UI-friendly:- single-select:
string | "" - multi-select:
string[]
- single-select:
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
- by
- minDate
- by
field.min
- by
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)
- by
- minSize
- by
field.min - minimum attachment count
- by
- maxSize
- by
field.maxCount - maximum attachment count
- by
- allowedContentTypes
- by
field.allowedContentTypes - validates attachment content types (e.g. jpg, png, pdf…)
- by
- maxAttachmentSize
- by
field.maxAttachmentSize - validates each attachment (file) size in Bytes
- by
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.