Reactable Forms API
FormBuilders
Reactable form builders are inspired by Angular Reactive Forms.
Forms are built by composing configuration objects using the helper functions: control
, group
, and array
.
Once composed, you initialize a form with the build
function.
build
build
is a factory function for creating a Form Reactable from a configuration object generated by control
, group
, or array
.
You can also provide an optional RxFormOptions
object to customize behavior.
Example
import { build, control, group, array, load } from "reactables";
// Simple form with a single control
const rxSimpleForm = build(control(["John Doe"]));
// Form with array of controls
const rxEmailsForm = build(
array({
controls: [
control(["john@example.com"]),
control(["doe@example.com"]),
],
})
);
// Form with nested group
const rxProfileForm = build(
group({
controls: {
firstName: control(["John"]),
lastName: control(["Doe"]),
},
})
);
// Subscribe to the profile form
const [state$, actions, actions$] = rxProfileForm;
state$.subscribe(formState => console.log("Profile form state:", formState));
actions.updateValues({ controlRef: ["firstName"] , value: "Jane", });
control
Creates a form control configuration object for a single field.
You can initialize it either with a configuration object or with a short-form tuple for convenience.
Example
// Using short-form tuple
const firstNameControl = control(["John", ['required']]);
// Using object notation with validators
const ageControl = control({
initialValue: 30,
validators: ["required", "number"],
normalizer: ["numbersOnly"]
});
Note: Validators are specified as strings here, but the actual functions must be provided via form options when initializing the form.
group
Creates a form group configuration object, which is a collection of controls, nested groups, or arrays.
The controls
dictionary contains keys for each child control.
You can also define validators and async validators at the group level, which apply to the group as a whole.
Example
const profileGroup = group({
controls: {
firstName: control(["John", ["required"]]),
lastName: control(["Doe"]),
age: control([30, ["required", "number"]]),
},
validators: ["groupValidator"],
asyncValidators: ["asyncGroupValidator"]
});
Note: Validators are specified as strings in the configuration; the actual functions must be provided via form options when initializing the form.
array
Creates a form array configuration, which is a list of controls, groups, or arrays.
Arrays are useful for repeated fields, such as multiple email addresses or phone numbers.
You can also define validators and async validators at the array level, which apply to the array as a whole.
Example
const emailsArray = array({
controls: [
control(["john@example.com", ["required"]]),
control(["doe@example.com"]),
],
validators: ["arrayValidator"],
asyncValidators: ["asyncArrayValidator"]
});
Note: Validators are specified as strings in the configuration; the actual functions must be provided via form options when initializing the form.
load
load
creates a Reactable form from a previously saved form state, allowing you to restore a form with existing values.
You can provide optional form options to customize behavior when loading.
Example
import { load } from "reactables";
// Load a saved form state from session storage
const savedState = JSON.parse(sessionStorage.getItem("profileForm") || "{}");
const rxLoadedForm = load(savedState);
const [state$, actions] = rxLoadedForm;
state$.subscribe(formState => console.log("Restored form state:", formState));
RxFormOptions
Options for customizing the behavior of a Reactable form.
You can provide additional action sources, custom reducers, validators, and other settings.
Option | Description |
---|---|
reducers | Dictionary of custom reducers for implementing special form behavior. Use built-in FormReducers to safely update form state while maintaining parent/child validation integrity. |
sources | An array of action observables the form should listen to. |
providers | See RxFormProviders for supplying validator, async validator, and normalizer functions. |
name | Optional name for the form. |
debug | Logs all form actions and state changes if true. |
Example
const formOptions = {
reducers: {
customReset: (formReducers, state) => ({ ...state, reset: true }),
},
sources: [externalActions$],
providers: rxProviders, // See RxFormProviders below
name: "profileForm",
debug: true
};
RxFormProviders
Provides reusable validators, async validators, and normalizer functions for form controls. Keys must match the names declared in controls created with control, group, or array.
Key | Description |
---|---|
normalizers | Dictionary of functions that transform input values before storing them in state. |
validators | Dictionary of synchronous validation functions to enforce rules on control values. |
asyncValidators | Dictionary of asynchronous validation functions. Each must be an observable operator function that returns an Observable<FormErrors> object. |
Example
const rxProviders = {
normalizers: {
trim: (value) => value.trim(),
numbersOnly: (value) => value.replace(/\D/g, "")
},
validators: {
required: (value) => !!value,
email: (value) => /\S+@\S+\.\S+/.test(value)
},
asyncValidators: {
uniqueUsername: (control$) =>
control$.pipe(
mergeMap(({ value }) =>
from(nameService.validateName(value)).pipe(
map((exists) => ({ uniqueUsername: exists }))
)
)
)
}
};
RxFormActions
Actions available to trigger state changes on Form Reactable.
updateValues
Updates the value of a control. For groups and arrays, only existing descendant controls are updated; otherwise it will throw an error.
Example
const rxForm = build(group({
controls: {
firstName: control(["John"]),
lastName: control(["Doe"]),
address: group({
controls: {
street: control(["123 Main St"]),
city: control(["Toronto"])
}
})
}
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form value:", state.root.value);
});
// Update top-level control
actions.updateValues({ controlRef: ["firstName"], value: "Jane" });
// Form value: { firstName: "Jane", lastName: "Doe", address: { street: "123 Main St", city: "Toronto" } }
// Update nested control
actions.updateValues({ controlRef: ["address", "street"], value: "456 Oak Ave" });
// Form value: { firstName: "Jane", lastName: "Doe", address: { street: "456 Oak Ave", city: "Toronto" } }
addControl
Adds a control to a form group at a specified key.
Example
const rxForm = build(group({
controls: {
name: control(["John Doe"])
}
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form value:", state.root.value);
});
// Add a new control dynamically
actions.addControl({
controlRef: ["email"],
config: control(["john@example.com", "email"])
});
// Form value: { name: "John Doe", email: "john@example.com" }
pushControl
Adds a control to the end of a form array.
Example
const rxForm = build(array({
controls: [control(["john@example.com"])]
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form value:", state.root.value);
});
// Push a new control into the array
actions.pushControl({
controlRef: [],
config: control(["doe@example.com"])
});
// Form value: ["john@example.com", "doe@example.com"]
removeControl
Removes a specified control from a group or array.
Example
const rxForm = build(group({
controls: {
firstName: control(["John"]),
lastName: control(["Doe"])
}
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form value:", state.root.value);
});
// Remove a control
actions.removeControl(["lastName"]);
// Form value: { firstName: "John" }
markControlAsPristine
Marks a control and all descendants as pristine, clearing any “dirty” state.
Example
const rxForm = build(group({
controls: {
profile: group({
controls: {
firstName: control(["John"]),
lastName: control(["Doe"])
}
}),
email: control(["john@example.com"])
}
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form value:", state.root.value, "| dirty:", state.root.dirty);
});
// Update nested control → both the control and ancestors become dirty
actions.updateValues({ controlRef: ["profile", "firstName"], value: "Jane" });
// Console: Form value: { profile: { firstName: "Jane", lastName: "Doe" }, email: "john@example.com" } | dirty: true
// Mark the whole form group as pristine → clears dirty flag for all descendants
actions.markControlAsPristine([]);
// Console: Form value: { profile: { firstName: "Jane", lastName: "Doe" }, email: "john@example.com" } | dirty: false
markControlAsTouched
Marks a control and all ancestors as touched.
Optional markAll flag marks all descendants as touched as well (default false
).
const rxForm = build(group({
controls: {
username: control(["JohnDoe"]),
password: control(["secret"])
}
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form status:", state.username.touched);
});
// LOGS: false
// Mark username as touched
actions.markControlAsTouched({ controlRef: ["username"] });
// LOGS: true
markControlAsUntouched
Marks a control and all descendants as untouched, and updates ancestor touched status accordingly.
Example
const rxForm = build(control(["hello"]));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form status:", state.root.touched);
});
// Mark as untouched
actions.markControlAsUntouched([]);
// LOG: false
resetControl
Resets a control by removing it and rebuilding it with the original configuration.
Example
const rxForm = build(group({
controls: {
city: control(["Toronto"])
}
}));
const [state$, actions] = rxForm;
state$.subscribe((state) => {
console.log("Form value:", state.root.value);
});
// Update city
actions.updateValues({ controlRef: ["city"], value: "Vancouver" });
// Form value: { city: "Vancouver" }
// Reset city to original config
actions.resetControl(["city"]);
// Form value: { city: "Toronto" }
Helpers
getArrayItems
Given a controlRef
for a form array and a Form
, returns all the controls for the form array control. If the controlRef
does not find a form array control an error is throw.
type getArrayItems = <T extends BaseForm<unknown> | Form<unknown>>(
controlRef: ControlRef,
form: T,
) => T extends BaseForm<unknown> ? BaseControl<unknown>[] : FormControl<unknown>[]
getAncestorControls
Given a controlRef
a Form
, returns all the ancestor controls including itself.
type getAncestorControls = <T extends BaseForm<unknown> | Form<unknown>>(
controlRef: ControlRef,
form: T,
excludeSelf = false,
) => (T extends Form<unknown> ? FormControl<unknown> : BaseControl<unknown>)[]
getDescendantControls
Given a controlRef
a Form
, returns all the descendant controls including itself.
type getDescendantControls = <T extends BaseForm<unknown> | Form<unknown>>(
controlRef: ControlRef,
form: T,
excludeSelf = false,
) => (T extends Form<unknown> ? FormControl<unknown> : BaseControl<unknown>)[]
getValueFromControlConfig
Reads a AbstractControlConfig
and returns its initial value for the form.
type getValueFromControlConfig = <T>(controlConfig: AbstractControlConfig) => T
Interfaces
Form
Form state. Dictionary of FormControl
(s) where the key is a period separated representation of the ControlRef
tuple.
interface Form<T> {
root: FormControl<T>;
[key: string]: FormControl<unknown>;
}
FormControl
interface FormControl<T> {
pristineValue: T;
controlRef: ControlRef;
value: T;
dirty: boolean;
touched: boolean;
validatorErrors: FormErrors;
key: string;
asyncValidatorErrors: FormErrors;
asyncValidateInProgress: { [key: string | number]: boolean };
errors: FormErrors;
valid: boolean;
childrenValid: boolean;
pending?: boolean;
config: AbstractControlConfig;
}
Property | Description |
---|---|
pristineValue | Original value of control. Use to determine if control is dirty. |
controlRef | Controls ControlRef . |
value | Control value. |
touched | Touched status of control |
validatorErrors | FormErrors from validators (non-async) |
asyncValidatorErrors | FormErrors from async validators |
errors | FormErrors validatorErrors and asyncValidatorErrors merged. |
valid | Valid status of control. Also checks descendants. |
childrenValid | Valid status of direct child controls. |
config | Original config for form control |
ControlRef
Control Reference represented as a tuple for the FormControl
FormErrors
Dictionary of errors for the control.
interface FormErrors {
[key: string]: any;
}
ValidatorFn
Validator function that reads the value of the FormControl
and returns a FormErrors
object.
type ValidatorFn = (value: any) => FormErrors;
ValidatorFnAsync
Validator function takes in an BaseControl
observable and returns a higher order observable Observable<Observabe<FormErrors>>
.
type ValidatorAsyncFn = <T>(control$: Observable<BaseControl<T>>) => Observable<Observable<FormErrors>>;
FormReducers
Built in reducers which can be used to update the state of the form tree. Payload and behaviour is the same and described in RxActions
;
interface FormReducers {
updateValues: <T>(state: BaseFormState<T>, payload: UpdateValuesPayload<unknown>,
) => BaseFormState<T>;
removeControl: <T>(state: BaseFormState<T>, payload: ControlRef) => BaseFormState<T>;
pushControl: <T>(state: BaseFormState<T>, payload: PushControlPayload) => BaseFormState<T>;
addControl: <T>(state: BaseFormState<T>, payload: AddControlPayload) => BaseFormState<T>;
markControlAsPristine: <T>(state: BaseFormState<T>, payload: ControlRef) => BaseFormState<T>;
markControlAsTouched: <T>(state: BaseFormState<T>, payload: MarkTouchedPayload) => BaseFormState<T>;
markControlAsUntouched: <T>(state: BaseFormState<T>, payload: ControlRef,
) => BaseFormState<T>;
resetControl: <T>(state: BaseFormState<T>, payload: ControlRef) => BaseFormState<T>;
}
CustomReducer
type CustomReducerFunc<FormValue = unknown> = (
reducers: FormReducers,
state: BaseFormState<FormValue>,
action: any,
) => BaseFormState<unknown>;
type CustomReducer<FormValue = unknown> =
| CustomReducerFunc<FormValue>
| {
reducer: CustomReducerFunc<FormValue>;
effects?: Effect[] | ((payload?: unknown) => ScopedEffects);
};
BaseFormState
Form state before it is fully validated. This is accessible in CustomReducer
s so developer can read the current state and implement custom form behaviours.
interface BaseFormState<T> {
form: BaseForm<T>;
_changedControls?: {
[key: string]: BaseControl<unknown>;
};
_removedControls?: {
[key: string]: BaseControl<unknown>;
};
}
type BaseForm<T> = {
root: BaseControl<T>;
[key: string]: BaseControl<unknown>;
};
Configuration Interfaces
interface ValidatorConfigs {
validators?: string[];
asyncValidators?: string[];
}
export interface FormGroupConfig extends ValidatorConfigs {
controls: { [key: string]: AbstractControlConfig };
}
export interface FormArrayConfig extends ValidatorConfigs {
controls: AbstractControlConfig[];
}
export interface FormControlConfig<T> extends ValidatorConfigs {
initialValue: T;
normalizers?: string[];
}
export type AbstractControlConfig = (
| FormControlConfig<unknown>
| FormArrayConfig
| FormGroupConfig
) & {
controls?: AbstractControlConfig[] | { [key: string]: AbstractControlConfig };
};