Skip to content

Mapped Category

The mapped-category field solves a common ML problem: models often expect one-hot encoded or structured inputs (e.g., is_red, is_green, is_blue), but exposing those raw fields to users creates bad UX and allows impossible combinations. Instead of showing N separate inputs, mapped-category presents a single dropdown and silently writes the correct values to hidden subordinate fields.

By default, the mapped-category field itself is not submitted. The backend receives only the subordinate fields unless you explicitly set includeInSubmission: true.

  1. User selects an option from the dropdown (renders identically to a regular category field).
  2. The engine reads the selected option’s mapping object.
  3. For each entry in mapping, the engine writes the value to the corresponding subordinate field via commitState.
  4. All updates happen inside a single store.batch — subscribers see one atomic state change.
User selects "Red"
→ mapping: { is_red: 1, is_green: 0, is_blue: 0 }
→ is_red = 1, is_green = 0, is_blue = 0
→ all written atomically
{
kind: "mapped-category",
label: "Color",
options: [
{ label: "Red", value: "red", mapping: { is_red: 1, is_green: 0, is_blue: 0 } },
{ label: "Green", value: "green", mapping: { is_red: 0, is_green: 1, is_blue: 0 } },
{ label: "Blue", value: "blue", mapping: { is_red: 0, is_green: 0, is_blue: 1 } },
],
}

Every option must be an object with label, value, and mapping:

PropertyTypeDescription
labelstringDisplay text shown in the dropdown.
valuestringInternal value stored when selected.
mappingRecord<string, unknown>Map of fieldId → value to apply on selection.

Unlike regular category, plain string options are not allowed — the mapping property is required.

mapped-category supports all shared field options (id, label, description, required, defaultValue, hiddenWhen, disabledWhen, readonlyWhen, ui, includeInSubmission).

Fields referenced in mapping are called subordinate fields. They typically should be:

  • Hidden (hidden: true or via hiddenWhen) — users don’t need to see or interact with them.
  • inactiveFieldPolicy: "include" — so their values are included in the submission payload even when hidden. Without this, hidden fields are omitted by default.
{ kind: "number", id: "is_red", label: "is_red", hidden: true, inactiveFieldPolicy: "include" }
{ kind: "number", id: "is_green", label: "is_green", hidden: true, inactiveFieldPolicy: "include" }
{ kind: "number", id: "is_blue", label: "is_blue", hidden: true, inactiveFieldPolicy: "include" }

The engine performs two levels of validation to catch configuration errors early:

When createForm() is called, for every mapped-category field the engine checks that every mapping key references an existing field ID in the schema. If a target ID doesn’t exist, it throws:

EngineError: mapped-category "color": mapping references unknown field "is_purple".

When a mapped value is applied, the engine coerces it through the target field’s coerceValue and then runs the target field definition’s validate function. If the value is incompatible, it throws:

EngineError: mapped-category "color": value "hello" invalid for "is_red": Expected a number.

This ensures you can’t accidentally map a string to a number field, an invalid option to a category field, etc.

Example 1: One-Hot Encoding (Classification)

Section titled “Example 1: One-Hot Encoding (Classification)”

A model trained on color categories expects three binary features:

const schema = {
fields: [
{
kind: "mapped-category",
id: "color",
label: "Color",
required: true,
options: [
{ label: "Red", value: "red", mapping: { is_red: 1, is_green: 0, is_blue: 0 } },
{ label: "Green", value: "green", mapping: { is_red: 0, is_green: 1, is_blue: 0 } },
{ label: "Blue", value: "blue", mapping: { is_red: 0, is_green: 0, is_blue: 1 } },
],
},
{ kind: "number", id: "is_red", label: "is_red", hidden: true, inactiveFieldPolicy: "include" },
{
kind: "number",
id: "is_green",
label: "is_green",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "is_blue",
label: "is_blue",
hidden: true,
inactiveFieldPolicy: "include",
},
{ kind: "number", id: "size", label: "Size (cm)", min: 1, max: 100, required: true },
],
reports: [{ kind: "classifier" }],
};

When user selects “Green”, the submission payload includes:

{ "is_red": 0, "is_green": 1, "is_blue": 0, "size": 42 }

The master field value (color: "green") is omitted by default. If your backend needs both forms, set includeInSubmission: true on the mapped-category field.

Example 2: Subscription Tier (Multi-Field Mapping)

Section titled “Example 2: Subscription Tier (Multi-Field Mapping)”

A pricing model needs multiple parameters per tier. Instead of separate dropdowns for each feature:

const schema = {
fields: [
{
kind: "mapped-category",
id: "plan",
label: "Subscription Plan",
required: true,
options: [
{
label: "Free",
value: "free",
mapping: { max_users: 1, storage_gb: 5, has_api: 0, has_support: 0 },
},
{
label: "Pro",
value: "pro",
mapping: { max_users: 10, storage_gb: 50, has_api: 1, has_support: 0 },
},
{
label: "Enterprise",
value: "enterprise",
mapping: { max_users: 100, storage_gb: 500, has_api: 1, has_support: 1 },
},
],
},
{
kind: "number",
id: "max_users",
label: "Max Users",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "storage_gb",
label: "Storage (GB)",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "has_api",
label: "API Access",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "has_support",
label: "Priority Support",
hidden: true,
inactiveFieldPolicy: "include",
},
{ kind: "number", id: "annual_revenue", label: "Annual Revenue ($)", required: true },
],
reports: [{ kind: "regressor" }],
};

Two mapped-category fields can coexist, each controlling separate subordinate fields:

const schema = {
fields: [
{
kind: "mapped-category",
id: "region",
label: "Region",
options: [
{
label: "North America",
value: "na",
mapping: { continent_na: 1, continent_eu: 0, continent_asia: 0 },
},
{
label: "Europe",
value: "eu",
mapping: { continent_na: 0, continent_eu: 1, continent_asia: 0 },
},
{
label: "Asia",
value: "asia",
mapping: { continent_na: 0, continent_eu: 0, continent_asia: 1 },
},
],
},
{
kind: "mapped-category",
id: "season",
label: "Season",
options: [
{ label: "Spring", value: "spring", mapping: { q1: 0, q2: 1, q3: 0, q4: 0 } },
{ label: "Summer", value: "summer", mapping: { q1: 0, q2: 0, q3: 1, q4: 0 } },
{ label: "Autumn", value: "autumn", mapping: { q1: 0, q2: 0, q3: 0, q4: 1 } },
{ label: "Winter", value: "winter", mapping: { q1: 1, q2: 0, q3: 0, q4: 0 } },
],
},
// Region subordinates
{
kind: "number",
id: "continent_na",
label: "continent_na",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "continent_eu",
label: "continent_eu",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "continent_asia",
label: "continent_asia",
hidden: true,
inactiveFieldPolicy: "include",
},
// Season subordinates
{ kind: "number", id: "q1", label: "Q1", hidden: true, inactiveFieldPolicy: "include" },
{ kind: "number", id: "q2", label: "Q2", hidden: true, inactiveFieldPolicy: "include" },
{ kind: "number", id: "q3", label: "Q3", hidden: true, inactiveFieldPolicy: "include" },
{ kind: "number", id: "q4", label: "Q4", hidden: true, inactiveFieldPolicy: "include" },
// Regular input
{ kind: "number", id: "sales", label: "Sales Volume", required: true },
],
reports: [{ kind: "regressor" }],
};

Subordinate fields don’t have to be numbers. You can map to any field type, as long as the value is valid for that field:

const schema = {
fields: [
{
kind: "mapped-category",
id: "preset",
label: "Model Preset",
options: [
{
label: "Conservative",
value: "conservative",
mapping: { risk_tolerance: "low", leverage: 1, stop_loss: 5 },
},
{
label: "Balanced",
value: "balanced",
mapping: { risk_tolerance: "medium", leverage: 3, stop_loss: 10 },
},
{
label: "Aggressive",
value: "aggressive",
mapping: { risk_tolerance: "high", leverage: 10, stop_loss: 25 },
},
],
},
{
kind: "category",
id: "risk_tolerance",
label: "Risk Tolerance",
hidden: true,
inactiveFieldPolicy: "include",
options: ["low", "medium", "high"],
},
{
kind: "number",
id: "leverage",
label: "Leverage",
hidden: true,
inactiveFieldPolicy: "include",
},
{
kind: "number",
id: "stop_loss",
label: "Stop Loss (%)",
hidden: true,
inactiveFieldPolicy: "include",
},
{ kind: "number", id: "portfolio_value", label: "Portfolio Value ($)", required: true },
],
reports: [{ kind: "regressor" }],
};

Combine mapped-category with hiddenWhen to show subordinate fields only when a condition is met — useful for debugging or advanced mode:

const schema = {
fields: [
{ kind: "boolean", id: "show_details", label: "Show Feature Details" },
{
kind: "mapped-category",
id: "material",
label: "Material",
required: true,
options: [
{ label: "Steel", value: "steel", mapping: { density: 7.8, conductivity: 50 } },
{ label: "Aluminum", value: "aluminum", mapping: { density: 2.7, conductivity: 205 } },
{ label: "Copper", value: "copper", mapping: { density: 8.9, conductivity: 385 } },
],
},
{
kind: "number",
id: "density",
label: "Density (g/cm³)",
inactiveFieldPolicy: "include",
hiddenWhen: { kind: "field-value", field: "show_details", notEquals: true },
readonlyWhen: { kind: "field-value", field: "show_details", equals: true },
},
{
kind: "number",
id: "conductivity",
label: "Thermal Conductivity (W/m·K)",
inactiveFieldPolicy: "include",
hiddenWhen: { kind: "field-value", field: "show_details", notEquals: true },
readonlyWhen: { kind: "field-value", field: "show_details", equals: true },
},
{ kind: "number", id: "thickness", label: "Thickness (mm)", required: true },
],
reports: [{ kind: "regressor" }],
};

Here, density and conductivity are hidden by default but become visible (and read-only) when the user toggles “Show Feature Details”. The mapping still applies regardless of visibility.

Mapped-category effects also trigger when you set values programmatically:

form.setValues({ color: "green" });
// → is_red = 0, is_green = 1, is_blue = 0
const values = form.getValues();
console.log(values.is_red); // 0
console.log(values.is_green); // 1
console.log(values.is_blue); // 0
  • UI: Renders as a standard <mlf-category-field> — no new web component needed.
  • Mapping direction: Unidirectional. Master → subordinates only. Changing a subordinate field directly does not update the master.
  • Atomicity: All mapping writes happen in a single batch. Subscribers receive one notification.
  • Submission: mapped-category is excluded from submission by default; set includeInSubmission: true to submit both the master and subordinate values.
  • inactiveFieldPolicy: Subordinate fields should use "include" to appear in submission payloads when hidden.
  • Validation: Target field IDs are validated at form creation. Mapped values are validated against target field definitions at runtime.
  • No cycles: The engine does not detect cycles between mapped-category fields. Avoid mapping one mapped-category to another.