tuain-ng-forms-lib
v17.4.0
Published
Componentes y Clases Angular para la gestión de formularios TUAIN
Readme
tuain-ng-forms-lib
UI-agnostic core of the Tuain platform's dynamic forms framework for Angular 17+.
It lets you build form-driven applications by rendering complete forms —fields, actions, tables and sections— from JSON definitions sent by the server, using a reactive programming model: each form is a class that declares how it reacts to UI events through callbacks, while the library takes care of instantiating the model, keeping the view in sync and orchestrating backend communication.
This library provides the domain model, the abstract base components and the services. It does not impose a UI library: concrete widgets are supplied by UI packages (e.g.
tuain-ng-forms-antwith ng-zorro,tuain-ng-forms-ionicwith Ionic) or by the application itself, by extending the base components.
Table of contents
- Purpose and architecture
- Services
- Forms API and the reactive approach
- Form lifecycle
- Form composition and element access
- Model objects vs. view components
- Using the library in an Angular project
1. Purpose and architecture
The goal is to leverage the construction of form-based applications: instead of hand-coding every screen, the server describes the form (fields, actions, tables, sections, states) as JSON and the library materializes it as a graph of live objects with reactive behavior. The application only writes the form's business logic (what to do when a field is validated, an action is executed, etc.).
The architecture is organized in layers:
┌──────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER (concrete UI) │
│ tuain-ng-forms-ant (ng-zorro) · tuain-ng-forms-ionic (Ionic)·App │
│ Widgets that extend the base components and supply the template │
├──────────────────────────────────────────────────────────────────┤
│ BASE COMPONENTS LAYER (abstract, standalone) │
│ FieldComponent · ActionComponent · LibTableComponent · … │
│ Sync model→view via signals; translate UI events │
├──────────────────────────────────────────────────────────────────┤
│ MODEL LAYER (domain) │
│ FormStructureAndData · FieldDescriptor · FormAction · RecordTable │
│ Form structure + data + state (live objects) │
├──────────────────────────────────────────────────────────────────┤
│ SERVICES LAYER │
│ FormManager · EventManager · FileManagement · IconResolver · SSE │
│ Server definition/actions, navigation, events, files │
└──────────────────────────────────────────────────────────────────┘
│
▼
Angular 17+ · RxJSModel hierarchy (each level adds capabilities):
FormPiece visibility, enablement, states, customAttributes
└─ FormPiecePropagate reactive attribute propagation (BehaviorSubject)
├─ FormElement elementType + setAttr() with propagation
│ ├─ FieldDescriptor field: value, validation, options, errors
│ ├─ FormAction action: backend, inProgress, restrictions
│ └─ RecordTable table: columns, records, paging, filters
├─ RecordFormSection section (activation/navigation)
└─ RecordFormSubSection subsection (groups elements)
FormStructureAndData form container (fields + actions + tables + state)
└─ BasicFormComponent Angular component: lifecycle + callbacks + serverEvery public entity also has a contract interface (IForm, IField, IAction, ITable, ITableColumn, ITableAction, ISection, …) implemented by the classes; applications may type against the contracts instead of the concrete classes.
2. Services
Services decouple the core from each application's specifics. They are provided via DI; some are abstract by design (the app implements them).
| Service / Token | Role | App implements? |
| --- | --- | --- |
| LibFormManagerService | Fetches the form definition (getFormDefinition), runs server actions (execServerAction), navigates between forms and manages the navigation stack (openForm, backTo, stack/unstack). | Yes (overrides the server/navigation methods) |
| LibEventManagerService | The app's event bus (Subject / BehaviorSubject / ReplaySubject) for communication between forms and components. | Instantiated with its set of events |
| LibFileManagementService | File handling (openFile, saveFile, saveFileFromURL, printPdfFile) per platform (web/mobile). | Yes |
| BaseIconResolverService + ICON_RESOLVER | Extensible, multi-collection icon resolution (each UI registers its collections). | Optional (extend/register) |
| SseLiveConnectionService + SSE_LIVE_CONNECTION_CONFIG | Server→client event channel over Server-Sent Events (native reconnection). All integration is provided through the config token. | Provides the config |
The core never assumes a concrete implementation: it defines the contract and the app satisfies it via providers.
3. Forms API and the reactive approach
An application form is a class that extends BasicFormComponent. The central idea of the reactive approach is that you don't program the flow imperatively; instead you declare how the form reacts to UI events, registering callbacks that the library invokes at the right moment.
UI components don't call business logic directly: they translate the UI event into a notification on the model object, and BasicFormComponent —subscribed to that object— fires the callbacks the app registered.
User types in the input
│ (template) (ngModelChange)
▼
FieldComponent.inputChanged()
├─ updateObject() → field.setValue(...) (view → model)
└─ onChangeContent() → field.notifyEditionFinish()
│ emits on editionFinish (Subject)
▼
BasicFormComponent.startFieldValidation(code)
│
▼
callbacks registered with onFieldValidationStart(...)
│ (if ok and field.backend)
▼
requestFormAction('VALIDATE') → updateFormWithServerData()
▼
callbacks registered with onFieldValidationFinish(...)Conversely, when the model changes (by code or by a server response) the view updates itself thanks to reactive propagation:
field.setValue(x) → setAttr() → propagateAttribute('value', x)
│ │ _attributeChange.next(...) (BehaviorSubject)
│ ▼
│ FieldComponent (subscribed to field.attributeChange)
│ ▼
│ this.value.set(x) (signal) → templateRegistering callbacks (how the form "reacts")
Inside start() (or preStart()) the app registers the handlers:
// Actions
this.onActionStart('save', (action) => this.validateBeforeSaving()); // false cancels
this.onActionFinish('save', (action, result) => this.notifySaved());
// Fields
this.onFieldValidationStart('email', (field) => this.preValidateEmail(field));
this.onFieldValidationFinish('email', (field) => this.afterValidateEmail(field));
this.onFieldInput('phone', (field) => this.formatWhileTyping(field));
// Tables
this.onTableActionStart('items', 'edit', (detail) => this.openEdition(detail));
this.onTableGetDataFinish('items', (detail, result) => this.afterPaging(detail));
// Sections and form
this.onSectionActivation('data', (section) => this.onEnterSection(section));
this.onFormChange((state) => this.onStateChanged(state));
// Server errors
this.onActionServerError((action) => this.showError());Every callback that runs before the backend (...Start) can return false to cancel the flow. The ...Finish ones run after the server part.
4. Form lifecycle
1. Angular creates the form component (a BasicFormComponent subclass)
2. ngOnInit() ─────────────► preStart() (config hook: formCode, route)
3. The app calls formInit(params):
a. processInputParams() extracts token / subject / state from the stack
b. getFormDefinition(name) requests the JSON from the server (FormManager)
c. loadDefinition(json) ──────────► instantiates FieldDescriptor / FormAction /
RecordTable / RecordFormSection (live model)
d. subscribeSectionActivation()
subscribeFieldsSubjects() the library subscribes to each model object's
subscribeActionSubjects() events and connects its state to the form's
subscribeTableSubjects() (connectWithParentForm)
e. changeState(initialState) state machine (visibility/editability)
f. requestFormAction('GETDATA') initial data load (if loadInitialData)
g. start() app hook: callbacks are registered HERE
4. Operation: the UI fires events → notifications on the model → callbacks
5. Angular destroys the component → takeUntilDestroyed cleans up the subscriptionsKey points:
preStart(): configurename(the form code) and route parameters.start(): register the callbacks (on*). Runs once the model is already loaded.changeState(state): the state machine defines, for each state, which fields/actions/tables are visible and editable (visibleStates/enabledStates). Changing state re-propagates visibility and enablement to the whole view.requestFormAction(code): the single point of backend communication; its response enters throughupdateFormWithServerData()and updates the existing fields/actions/tables viaupdateFromServer().
5. Form composition and element access
FormStructureAndData (which BasicFormComponent extends) holds the form graph and exposes a rich API to access and manipulate its elements. Each element is obtained by its code and operated directly on the model object.
// Fields → FieldDescriptor (IField)
const email = this.getField('email');
email.setValue('[email protected]');
email.required = true;
if (email.hasError()) { /* … */ }
this.getFieldValue('email'); // shortcut
this.getRequiredEmptyFields(null, 'data'); // by section
// Actions → FormAction (IAction)
const save = this.getAction('save');
this.disableAction('save');
this.getHeaderActions();
// Tables → RecordTable (ITable), with columns and table actions
const items = this.getTable('items');
items.setTableRecords(records);
items.getActions('INLINE'); // ITableAction[]
items.columns; // ITableColumn[]
const rec = this.getTableRecord('items', id);// ITableRecord
// Sections → RecordFormSection (ISection) / RecordFormSubSection (ISubSection)
this.activateSection('data');
this.getSubSection('data', 'personal');Element ↔ component relationship:
| Element (model) | Contract | Base view component |
| --- | --- | --- |
| FieldDescriptor | IField | FieldComponent |
| FormAction | IAction | ActionComponent |
| RecordTable | ITable | LibTableComponent |
| RecordTableColumn / TableAction | ITableColumn / ITableAction | cells in LibTableRecordFieldComponent / LibTableRecordActionComponent |
| RecordFormSection / RecordFormSubSection | ISection / ISubSection | SectionComponent / SubSectionComponent |
| FormStructureAndData | IForm | BasicFormComponent / FormHeaderComponent |
Each component receives its model object via @Input() ([field], [action], [table], …), registers itself as that object's widget and subscribes to its changes to reflect them; and it translates user events into notifications on that same object. In other words, the component is a view of the model object, not its owner.
6. Model objects vs. view components
This is the most important distinction for using the library correctly:
MODEL (lives for the whole form) VIEW (ephemeral, created/destroyed by Angular)
┌───────────────────────────┐ ┌───────────────────────────┐
│ FieldDescriptor 'email' │◄── @Input ────│ FieldComponent │
│ (created in loadDefinition│ field.widget │ (created when rendered) │
│ lives until the form is │◄── subscription│ signals: value(), … │
│ closed) │ attributeChange destroyed when hidden │
└───────────────────────────┘ └───────────────────────────┘
▲ source of truth ▲ recreated without losing data
│ getField('email') always returns the same object- Model objects (
FieldDescriptor,FormAction,RecordTable, …) are created once inloadDefinition()and live for as long as the form exists. They hold the state: value, errors, visibility, selection, etc.getField('x')always returns the same instance. - Components (
FieldComponent, etc.) are ephemeral: Angular creates and destroys them based on what is currently shown (section change,@if, table paging, virtual scroll…). When destroyed and recreated, nothing is lost: the new component re-subscribes to the model object and recovers the current state.
Practical consequences:
- The form's business logic operates on the model objects (
this.getField('x').setValue(...)), never on the components. It works even if the component is not mounted. - Don't keep state in the component expecting it to persist; state lives in the model.
- A model object and its component are not the same thing: the component is a replaceable reactive projection of the object, whereas the object is the stable, unique entity.
7. Using the library in an Angular project
Installation
npm install tuain-ng-forms-lib
# or: yarn add tuain-ng-forms-libPeer dependencies: Angular ^17 (common, core, forms, router) and rxjs ^7.5.
a. Provide the services
Standalone apps register them in main.ts (or in an NgModule with providers):
import { bootstrapApplication } from '@angular/platform-browser';
import { LibFormManagerService, LibFileManagementService, LibEventManagerService } from 'tuain-ng-forms-lib';
import { AppFormManager } from './services/app-form-manager.service'; // your implementation
import { AppFileManager } from './services/app-file-manager.service';
bootstrapApplication(AppComponent, {
providers: [
{ provide: LibFormManagerService, useClass: AppFormManager },
{ provide: LibFileManagementService, useClass: AppFileManager },
{ provide: LibEventManagerService, useValue: new LibEventManagerService(['formActivity', /* … */]) },
// optional: ICON_RESOLVER, SSE_LIVE_CONNECTION_CONFIG
],
});AppFormManager extends LibFormManagerService and implements the bridge to your backend:
@Injectable()
export class AppFormManager extends LibFormManagerService {
override async getFormDefinition(formCode: string): Promise<any> { /* HTTP → JSON */ }
override async execServerAction(detail: any): Promise<any> { /* HTTP → response */ }
override goToForm(formCode: string, token: string, subject: string | null): void { /* router */ }
}b. Define a form
import { Component } from '@angular/core';
import { BasicFormComponent, LibFormManagerService, LibEventManagerService, LibFileManagementService } from 'tuain-ng-forms-lib';
import { appFormConfig } from './app-form.config'; // your IFormConfig
@Component({ standalone: true, selector: 'app-customer-form', templateUrl: './customer-form.html' })
export class CustomerFormComponent extends BasicFormComponent {
constructor(fm: LibFormManagerService, em: LibEventManagerService, fs: LibFileManagementService) {
super(fm, em, fs);
this.setConfig(appFormConfig);
}
override preStart(): void {
this.name = 'CUSTOMER'; // form code
this.formInit({ /* route params */ }); // triggers load + initialization
}
override start(): void {
// Declare HOW the form reacts:
this.onFieldValidationStart('documentId', (f) => this.validateDocument(f));
this.onActionStart('save', () => this.validateSectionConsistency('data'));
this.onActionFinish('save', () => this.goBack());
}
private validateDocument(field /* IField */): boolean {
if (field.empty) { field.setErrorMessage('Required'); return false; }
return true;
}
}c. Render
The core ships abstract base components: to show concrete widgets you use a Tuain UI package (e.g. tuain-ng-forms-ant / tuain-ng-forms-ionic) or your own components that extend FieldComponent, ActionComponent, LibTableComponent, etc. and supply their template. In the form template you bind each component to its model object:
<lib-form-header [form]="form" (goBackEvent)="goBack()"></lib-form-header>
<app-field *ngFor="let f of getFields()" [field]="f"></app-field>
<app-action *ngFor="let a of getHeaderActions()" [action]="a"></app-action>
<app-table *ngFor="let t of getTables()" [table]="t"></app-table>Until the UI packages are available, the app defines its own field/action/table components by extending this library's base components and overriding
start()/focus()and the template.
Public API
Exported (from tuain-ng-forms-lib):
- Model:
FormStructureAndData,FieldDescriptor,FormAction,RecordTable,RecordTableColumn,TableAction,TableRecordData,RecordFormSection,RecordFormSubSection, and the basesFormPiece/FormPiecePropagate/FormElement. - Contracts:
IForm,IField,IAction,ITable,ITableColumn,ITableAction,ITableRecord,ISection,ISubSection,IFormPiece/IFormElement,IFormConfigand the definition/event types. - Base components (standalone):
BasicFormComponent,FieldComponent,ActionComponent,LibTableComponent,LibTableRecordFieldComponent,LibTableRecordActionComponent,SectionComponent,SubSectionComponent,FormHeaderComponent,FormErrorComponent,ElementComponent,PieceComponent. - Services and tokens:
LibFormManagerService,LibEventManagerService,LibFileManagementService,BaseIconResolverService+ICON_RESOLVER,SseLiveConnectionService+SSE_LIVE_CONNECTION_CONFIG.
