@bolttech/form-engine-core
v1.0.3
Published
Development maintenance documentation
Maintainers
Keywords
Readme
Form Engine Core
Development maintenance documentation
- Changing private field class properties implementation
- Value change Rules
- Props change and visibility rules
- Error messages display
- subject dependencies
- 6.1 field
- 6.1.1 templateSubject$
- 6.1.2 fieldEventSubject$
- 6.1.3 dataSubject$
- 6.1.4 formValidNotification$
Arquitecture Overview

Form-engine basic arquitecture is class based, composed by FormGroup, Form and Field classes and the communication is Observable Based in order to feed the adapters that will be the responsible to emit and recieve the reactive manipulations configured on the schema.
Each class is independent from bottom to top, meaning that you can instanciate a Field independently, but a Form relies on Fields to work, because a Form class manages Fields that interact with each others, same as FormGroup that relies on Forms in order to work.
FormGroup manages forms stored as a Map of key/value, same with Form that stores fields as a Map of key/value.
FormGroup responsability
- manage the add and removal of forms by it's key
- manage data inserted by user onto forms to provide callbacks to the adapter
- manage validity of forms to provide callbacks to the adapter
- work is in progress to check which functionalities it needs
FormCore responsability
- manage the add and removal of fields by it's key
- manage visibility of fields based on schema configuration
- manage resetValues based on schema configuration
- manage templating properties change emitted across fields
- manage component configuration based on mapper list config
- manage callback subscription on events emmited by fields (adapter external actions trigger)
- manage form values submission and validity check
FormField responsability
- manage storage and emission of properties such as validations, value, error messages
- manage api schema configurations for requests
- manage initialization of adapters external configuration binding generalization
Changing private field class properties implementation
On the process of implementation, it's crucial to understand that the reactivity of the form relies on RXJS, the way this properties change can cause side effects on other properties and all are managed by RXJS subjects, so for all this class properties, each time they change, they need to emit to the corresponding subject, so all this properties have a subject emmited on the end.
If you work in any maintenance involving this properties:
private _props: Record<string, unknown>;
private _value: unknown;
private _stateValue: Record<string, unknown>;
private _metadata: unknown;
private _visibility: boolean;
private _errors: TErrorMessages;
private _api: TApiResponse;
private _valid: boolean;
private _mounted: boolean;they all have a get and set method constructors
The get constructor is responsible to deliver the value
The set constructor is responsible to set the value on this private properties along with emitting to the corresponding subject and the form template subject
private _stateValue: unknown;
private _metadata: unknown;
errorsString: string;
errorsList: string[];This properties are computed based on other private properties,
_stateValue has the mask applied to the _value when set value method is called
_metadata has the metadata sent by the valueChangeEvent mapper function when set value is called
when set errors method is called, errorsString is the errors message list concatenated in a string, separated by commas , and errorsList is the list of the errors message
If you try to change this values directly without using it's constructors, it can cause serious problems on the form reactivity, even in a last resort scenario, evaluate cautiously if you should change this values directly on any maintenance or feature implementation
Value change Rules
If you manage to work on value change, take in consideration that the value change is treated as a special property on the configuration, let's take in consideration what happens on a component in react:
const [value, setValue] = useState('');
const handleChange = (e) => {
const val = e.currentTarget.value;
val ? (setValue = val) : '';
};
<input onChange={handleChange} value={value} />;The value prop is binded to the value state and the onChange prop is binded to the function that will change this value.
In form-engine, the property that holds the value and the property that changes this value are:
class FormField
private _value: unknown;
(...)
get value(): unknown {
return this._value;
}
set value(value: unknown) {
/*
too much unstable, if the valueChangeEvent parses the template event
value, might occur unexpected results
*/
let val;
if (this.valueChangeEvent) {
try {
val = this.valueChangeEvent(value, { props: this.props });
} catch (e) {
val = value;
}
} else {
val = value;
}
if (typeof val === 'undefined' || val === null) return;
if (typeof val === 'object' && '_value' in val && '_metadata' in val) {
this._value = this.formatValue(val['_value']);
this._stateValue = this.mapper.events?.setValue
? {
[this.mapper.events.setValue]: this.maskValue(
this.formatValue(val['_value'])
),
}
: {};
this._metadata = val._metadata;
} else {
this._value = this.formatValue(val);
this._stateValue = this.mapper.events?.setValue
? {
[this.mapper.events?.setValue]: this.maskValue(
this.formatValue(val)
),
}
: {};
this.maskValue(this.formatValue(val));
this._metadata = val;
}
this.stateValue && this.valueSubject$.next(this.stateValue);
this.templateSubject$.next({ key: this.name, event: 'ON_VALUE' });
}In this case, the value passed to this setter method can have multiple formats (it's the valueChangeEvent mapper function that is responsible to pass the value on each of this formats):
value: unknown;The value is formatted and stored onto fieldinstance._value and the value prop name with the value into fieldinstance._stateValue
{ _value: unknown, _metadata: unknown }The _value key is formatted and stored onto fieldinstance._value and the value prop name with the value into fieldinstance._stateValue
The _metadata key is stored at fieldinstance._metadata as it is, and can be retrieved using fieldinstance.metadata readonly property
But, this change can have sideEffects defined on the Schema, like validations, visibilityConditions and so on, so proper way to change this value and trigger all side effects in order to update the component that is using this instance is using the fieldinstance.emitValue class function:
fieldinstance.emitValue({ event: 'ON_FIELD_CHANGE', value: 'hello' });This function will set the value along with triggering all the side effects and triggers the onData callbacks defined:
emitValue(prop: {
value: unknown | { _value: unknown; _stateValue: unknown };
event: TEvents;
}): void {
this.value = prop.value;
this.emitEvents({ event: prop.event });
this.dataSubject$.next({ key: this.name, event: prop.event });
}
emitEvents({ event }: { event: TEvents }): void {
this.setFieldValidity({ event });
this.validateVisibility({ event, key: this.name });
this.resetValue({ event, key: this.name });
this.apiEventQueueSubject$.next({ event });
this.fieldEventSubject$.next({
event,
fieldName: this.name,
fieldInstance: this,
});
}To emit a capture of this value change, value setter emits on the end, setting the value on the configured property on the mapper -> events -> setValue when the value is being retrieved, an object is created with the correspondent key configured to be passed as props on the component.
Also, there is an helper function to help register this callback function that is responsible to change this value on the adapter called subscribeValue.
So, to compare the basic react implementation from a simple binded state along with a change handler to a form-engine controlled input handler, the implementation goes like this:
const [valueState, setValueState] = useState<Record<string, unknown>>(fieldinstance.stateValue || {});
fieldInstance = new FormField(
(...)
mapper: {
events: {
setValue: 'value'
}
}
(...)
)
useEffect(() => {
fieldInstance.subscribeValue((value) => setValueState(value));
return () => {
fieldInstance?.destroyField();
};
}, []);
const handleChange = useCallback((event: unknown) => {
fieldInstance?.emitValue({ value: event, event: 'ON_FIELD_CHANGE' });
}, []);
return <input onChange={handleChange} {...valueState}>The listener is registered when the field mounts calling subscribeValue on the field instance that will update the state.
The handleChange calls the field instance emitValue
The rest of the process to manage the validation, visibility and so on occurs internally on form-engine-core, the only thing that the adapter needs to do is inform the value that is set as input, the output is handled and delivered on the callback function defined on the effect.
Note: this is for demonstration purposes, the final implementation of the adapter only resembles to this
Props change and visibility rules
Props change can be defined on the schema or can be changed with templating, let's take into consideration what happens in react:
return <input label={props.label} placeholder={props.placeholder}>React will rerender this component each time props are changed, in the case of form-engine, this props are passed on the schema and can have templating that will change based on any field side effect, what manages the props change is an RXJS subject, to properly register the props and let the form-engine manage the side effects, this is needed to be done:
const [state, setState] = useState<Partial<IState>>({
visibility: fieldInstance?.visibility || true,
props: fieldInstance?.props || props,
});
useEffect(() => {
fieldInstance.subscribeState((props) => {
setState((prev) => ({
...prev,
...props,
}))
})
return () => {
fieldInstance?.destroyField();
};
},[])
return {state.visibility && <input {...props}>}Field instance has an helper function to register the callback function subscribeState to be called each time props change internally on form-engine, and on this callback function, you need to change the state of the props in order to occur onto the adapter, also the visibility control is made by the visibility prop given by the callback function, so the condition to show or hide the component is made onto the adapter depending on this prop value.
NOTE: trying to pass the value sending the value prop will not work except if this value is a template, also, templates that will change based on the same field will not work either
Note: this is for demonstration purposes, the final implementation of the adapter only resembles to this
Error messages display
For error message display, based on validation logic occuring in the field configured onto the schema, the component need to have a property configured onto the mappers configuration onto mappers -> events -> setErrorMessage, then, each time an error occurs, the field will automatically have an error displayed on the corresponding prop.
Example of a basic component in react:
const [errorMessage, setErrorMessage] = useState('')
return <Input errorMessage={errorMessage}>In form-engine, the error message will be passed onto the subscribedState on errors with the prop name passed onto the mapper config and all the error messages that were triggered
Example:
fieldInstance = new FormField(
(...)
mapper: {
events: {
setErrorMessage: 'errorMessage'
}
}
(...)
)
const [state, setState] = useState<Partial<IState>>({
visibility: fieldInstance?.visibility || true,
props: fieldInstance?.props || props,
});
useEffect(() => {
fieldInstance.subscribeState((props) => {
setState((prev) => ({
...prev,
...props,
}))
})
return () => {
fieldInstance?.destroyField();
};
},[])
return <Input {...state.errors}>The error will contain an object with a key named errorMessage with all the errors that occured separated with a comma, the key configured onto the mapper varies based on the component props, if another component sets error messages on another prop, needs to be configured onto the mapper config onto setErrorMessage inside events.
Note: this is for demonstration purposes, the final implementation of the adapter only resembles to this
subject dependencies
field
templateSubject$: Subject<TTemplateEvent>;
fieldEventSubject$: Subject<TFieldEvent>;
dataSubject$: Subject<{ key: string; event: TEvents }>;
formValidNotification$: Subject<Pick<TFormValidationPayload, 'fieldTrigger'>>;this subjects needs to be passed as a field constructor parameters, and the field invoker needs to instanciate them, they are part of the form instance implementation
templateSubject$
This subject needs to be invoked on any field mutation, this will notify the form that a field changed and any other field that has a template dependency needs to be recomputed,
Ex: field1 label props depends on field2 value, so: field1.props.label has a template ${field2.props.value}, each time field2 value changes,
the templateSubject$ emits, the form gets the notification and checks that field2 has a field1 dependency and updates the label with the value
of field2
fieldEventSubject$
Each time a basic event mapped from a component occurs like onChange or onBlur the field emits this subject with it's event type and field name,
on form, if adapter invokes subscribeFieldEvent on an instanciated form with a callback function, this function is called, each time the field emitEvents is invoked, otherwise it's ignored
dataSubject$
This subject triggers each time emitValue is called, this subject handles field value changes on any event type, if the adapter invokes subscribeData on an instanciated form and passes a callback function, this functions will be executed each time a field emitValue is invoked
formValidNotification$
This subject triggers each time a field validation status change, if the adapter invokes subscribeFormValidation with a callback function on an instanciated form, this function will execute, each time a field validity changes, giving the form validity status
build fields with a schema
this is a brief explanation of how building fields with a schema works, the constructor method of the form takes care of the process if a schema is passed as a parameter
fields
every schema is parsed with the form instance serializeStructure method, this will pick all fields, instanciates all form fields on a map and they are ready to interact with the form with the subjects passed on the constructors, it's the responsability of the adapter to pick this fields map and build the component tree, you can check the react adapter BuildTree method
templates
to register the templates, the form instance subscribeTemplates needs to be invoked, this will create a list of dependencies to be checked each time a templateSubject$ is triggered, note that this needs to be called when all fields are instantiated, otherwise all templates might not be registered accordingly
build fields without schema
if you want to add a field without a schema, the form instance has addField method, this will add a field onto the form instance and handles a couple of pre requisites, like checking if the name doesn't exists and regists the templates, it's the responsability of the adapter to figure out how to render this field on
form group event callback register
FormGroup instance provides methods to add and remove form instances, also they let you regist onData and onValid events on form groups, the focus will be this two events that can be used by the adapters:
form group onData
formGroup onDataSubscription instance method let's you pass a list of indexes along with a callback function to be evoked each time any form instance emits onData, this is useful to handle groups of forms
form group onValid
formGroup onValidSubscription instance method let's you pass a list of indexes along with a callback function to be evoked each time any form changes it's validity status, this is useful to handle groups of forms, it will returns the form group validity status and each individual form validity status and it's triggered each time a form validity status changes
