Core Concepts

createFormHandler

The createFormHandler function is the heart of NovaForms' state management system. It creates a change handler that automatically manages form state, applies field modifiers, and executes rules when field values change.


Overview

createFormHandler returns a function that:

  • Manages form state updates
  • Applies legacy field-level modifiers
  • Executes top-level rules triggered by field changes
  • Handles both React synthetic events and direct value calls
  • Supports complex calculations and string operations

Basic Usage

import { createFormHandler } from "nova-forms";

const handleChange = createFormHandler({
  fields,        // Array of field definitions
  setState,      // React setState function
  rules          // Optional array of rules
});

Parameters

ParameterTypeRequiredDescription
fieldsArrayYesArray of field definitions
setStateFunctionYesReact setState function
rulesArrayNoArray of top-level rules

How It Works

1. Event Handling

The handler accepts both React synthetic events and direct value calls:

// React synthetic event (from input onChange)
handleChange(event);

// Direct value call (from custom components)
handleChange("new value", "fieldName");

2. State Management

The handler automatically updates form state:

const [formData, setFormData] = useState({});

const handleChange = createFormHandler({
  fields,
  setState: setFormData
});

// When a field changes, formData is automatically updated
// formData = { firstName: "John", lastName: "Doe", ... }

3. Rule Execution

When a field changes, the handler checks for triggers and executes matching rules:

const rules = [
  {
    name: "calculateTotal",
    effects: [
      {
        targetField: "total",
        type: "add",
        value: 10
      }
    ]
  }
];

const fields = [
  {
    name: "quantity",
    type: "number",
    triggers: [
      {
        rule: "calculateTotal",
        when: "not empty"
      }
    ]
  }
];

Legacy Modifiers

For backward compatibility, NovaForms still supports field-level modifiers:

const fields = [
  {
    name: "firstName",
    type: "string",
    modifiers: [
      {
        target: "fullName",
        type: "concat",
        when: "true",
        value: " ",
        strictString: true
      }
    ]
  }
];

Modifier Properties

PropertyTypeDescription
targetstringField name to modify
typestringOperation type (add, subtract, multiply, divide, concat, replace)
whenstringCondition when to apply
valueanyValue to use in operation
kindstringData type (number, string)
strictStringbooleanForce string operations

Rules System

The modern approach uses top-level rules with field-level triggers:

Rule Structure

const rules = [
  {
    name: "ruleName",           // Unique identifier
    effects: [                  // Array of effects to apply
      {
        targetField: "fieldName", // Field to modify
        prop: "value",           // Property to modify (value, readOnly, hidden, etc.)
        type: "add",             // Operation type
        kind: "number",          // Data type
        value: 10,               // Value for operation
        strictString: false,     // Force string operations
        sourceFields: []         // For concat operations
      }
    ]
  }
];

Effect Properties

PropertyTypeDescription
targetFieldstringField name to modify
propstringProperty to modify (value, readOnly, hidden, title, etc.)
typestringOperation type
kindstringData type (number, string)
valueanyValue for operation
strictStringbooleanForce string operations
sourceFieldsArrayFor concat operations with multiple fields

Operation Types

Math Operations

  • add: Addition
  • subtract: Subtraction
  • multiply: Multiplication
  • divide: Division
  • replace: Replace with new value

String Operations

  • concat: Concatenation
  • replace: Replace with new value

Special Operations

  • concat with sourceFields: Concatenate multiple fields with custom separators

Examples

Simple Calculation

const rules = [
  {
    name: "calculateTax",
    effects: [
      {
        targetField: "totalWithTax",
        type: "multiply",
        kind: "number",
        value: 1.08  // 8% tax
      }
    ]
  }
];

const fields = [
  {
    name: "subtotal",
    type: "number",
    triggers: [
      {
        rule: "calculateTax",
        when: "not empty"
      }
    ]
  },
  {
    name: "totalWithTax",
    type: "number",
    readOnly: true
  }
];

String Concatenation

const rules = [
  {
    name: "buildFullName",
    effects: [
      {
        targetField: "fullName",
        type: "concat",
        sourceFields: [
          { field: "firstName", charBefore: "", charAfter: " " },
          { field: "lastName", charBefore: "", charAfter: "" }
        ]
      }
    ]
  }
];

const fields = [
  {
    name: "firstName",
    type: "string",
    triggers: [
      {
        rule: "buildFullName",
        when: "not empty"
      }
    ]
  },
  {
    name: "lastName",
    type: "string",
    triggers: [
      {
        rule: "buildFullName",
        when: "not empty"
      }
    ]
  },
  {
    name: "fullName",
    type: "string",
    readOnly: true
  }
];

Complex Multi-Field Calculation

const rules = [
  {
    name: "calculateOrderTotal",
    effects: [
      {
        targetField: "subtotal",
        type: "multiply",
        kind: "number",
        value: 1
      },
      {
        targetField: "tax",
        type: "multiply",
        kind: "number",
        value: 0.08
      },
      {
        targetField: "total",
        type: "add",
        kind: "number",
        value: 0
      }
    ]
  }
];

const fields = [
  {
    name: "quantity",
    type: "number",
    triggers: [
      {
        rule: "calculateOrderTotal",
        when: "not empty"
      }
    ]
  },
  {
    name: "price",
    type: "number",
    triggers: [
      {
        rule: "calculateOrderTotal",
        when: "not empty"
      }
    ]
  },
  {
    name: "subtotal",
    type: "number",
    readOnly: true
  },
  {
    name: "tax",
    type: "number",
    readOnly: true
  },
  {
    name: "total",
    type: "number",
    readOnly: true
  }
];

Advanced Features

Conditional Rule Execution

Rules can have complex trigger conditions:

const fields = [
  {
    name: "discountCode",
    type: "string",
    triggers: [
      {
        rule: "applyDiscount",
        when: [
          { field: "discountCode", when: "matches", value: "SAVE10" },
          { field: "subtotal", when: "greater than", value: 50 }
        ],
        mode: "all"  // Both conditions must be true
      }
    ]
  }
];

Attribute Effects

Rules can modify field attributes, not just values:

const rules = [
  {
    name: "lockAgeField",
    effects: [
      {
        targetField: "age",
        prop: "readOnly",
        value: true
      }
    ]
  }
];

Best Practices

1. Use Rules Over Modifiers

Prefer the rules system over legacy modifiers for new forms:

// ✅ Good: Use rules
const rules = [
  {
    name: "calculateTotal",
    effects: [
      {
        targetField: "total",
        type: "add",
        value: 10
      }
    ]
  }
];

// ❌ Avoid: Legacy modifiers
const fields = [
  {
    name: "quantity",
    modifiers: [
      {
        target: "total",
        type: "add",
        value: 10
      }
    ]
  }
];

2. Keep Rules Focused

Each rule should have a single responsibility:

// ✅ Good: Focused rules
const rules = [
  {
    name: "calculateSubtotal",
    effects: [
      { targetField: "subtotal", type: "multiply", value: 1 }
    ]
  },
  {
    name: "calculateTax",
    effects: [
      { targetField: "tax", type: "multiply", value: 0.08 }
    ]
  }
];

// ❌ Avoid: Monolithic rules
const rules = [
  {
    name: "calculateEverything",
    effects: [
      { targetField: "subtotal", type: "multiply", value: 1 },
      { targetField: "tax", type: "multiply", value: 0.08 },
      { targetField: "total", type: "add", value: 0 }
    ]
  }
];

3. Use Descriptive Names

Make rule and field names descriptive:

// ✅ Good: Descriptive names
const rules = [
  {
    name: "calculateOrderTotalWithTax",
    effects: [...]
  }
];

// ❌ Avoid: Unclear names
const rules = [
  {
    name: "calc1",
    effects: [...]
  }
];

Troubleshooting

Common Issues

  1. Rules not executing: Check that triggers reference the correct rule name
  2. Calculations not working: Ensure strictString: false for numeric operations
  3. State not updating: Verify setState is the correct React setState function

Debug Tips

const handleChange = createFormHandler({
  fields,
  setState: (newData) => {
    console.log("Form data updated:", newData);
    setFormData(newData);
  },
  rules
});

The createFormHandler function is designed to handle complex form logic while keeping your components simple. Use it as the foundation for all your NovaForms implementations.

Previous
Fields & Schemas