@flusys/ng-form-builder
v1.0.0-rc
Published
Dynamic form builder for FLUSYS Angular applications
Readme
@flusys/ng-form-builder Package Guide
Overview
A frontend-only, fully dynamic Form Builder and Viewer system for Angular 21. Build forms visually, export/import JSON schemas, render forms dynamically, view submitted results, and export to PDF.
Package Information
| Property | Value |
|----------|-------|
| Package | @flusys/ng-form-builder |
| Dependencies | @flusys/ng-core, @flusys/ng-shared |
| Peer Dependencies | @angular/cdk, pdfmake (optional) |
| Build Command | ng build ng-form-builder |
Optional Dependencies
PDF Export (pdfmake): Required only if using PdfExportService.
npm install pdfmake @types/pdfmakeComponents
FormBuilderComponent
Visual drag-and-drop form creation interface.
Selector: fb-form-builder
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| schema | IFormSchema \| null | - | Schema to load |
| Output | Type | Description |
|--------|------|-------------|
| schemaChange | IFormSchema | Emits on every schema change |
| schemaSave | IFormSchema | Emits on save button click |
| schemaExport | string | Emits JSON string on export |
import { FormBuilderComponent } from '@flusys/ng-form-builder';
@Component({
imports: [FormBuilderComponent],
template: `
<fb-form-builder
[schema]="formSchema()"
(schemaChange)="onSchemaChange($event)"
(schemaSave)="onSave($event)"
(schemaExport)="onExport($event)"
/>
`
})
export class MyBuilderPage {
readonly formSchema = signal<IFormSchema | null>(null);
onSchemaChange(schema: IFormSchema): void {
console.log('Schema updated:', schema);
}
onSave(schema: IFormSchema): void {
this.apiService.saveForm(schema).subscribe();
}
onExport(json: string): void {
// JSON string of the schema
}
}Features:
- Three-panel layout (palette | canvas | properties)
- Drag & drop fields from palette to canvas
- Section mode: organize fields into sections with navigation
- Flat mode: simple form without sections
- Section management (add, edit, delete, reorder, duplicate)
- Field configuration panel with scrollable tabs (General, Validation, Options, Logic)
- Flex/Grid layout options per section with responsive breakpoint configuration
- JSON import/export with error notifications (via optional
MessageService) - Responsive design with mobile-optimized panels and padding
- Dark mode support via CSS variables
Note: If you want error notifications during JSON import, provide MessageService from PrimeNG in a parent component:
@Component({
providers: [MessageService],
imports: [FormBuilderComponent, ToastModule],
template: `<p-toast /><fb-form-builder ... />`
})FormViewerComponent
Runtime form rendering from JSON schema.
Selector: fb-form-viewer
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| schema | IFormSchema | required | Form schema to render |
| initialValues | Record<string, unknown> | {} | Pre-filled values |
| disabled | boolean | false | Disable all fields |
| showHeader | boolean | true | Show form title/description |
| showSubmit | boolean | true | Show submit button |
| showSaveDraft | boolean | true | Show save draft button |
| showCancel | boolean | false | Show cancel button |
| submitLabel | string | 'Submit' | Submit button text |
| cancelLabel | string | 'Cancel' | Cancel button text |
| isSubmitting | boolean | false | Show loading state on submit |
| Output | Type | Description |
|--------|------|-------------|
| submitted | Record<string, unknown> | Emits field values on submit |
| valueChanged | Record<string, unknown> | Emits all values on any change |
| validityChanged | boolean | Emits form validity state |
| saveDraft | Record<string, unknown> | Emits values on save draft |
| cancelled | void | Emits when cancel button is clicked |
import { FormViewerComponent } from '@flusys/ng-form-builder';
@Component({
imports: [FormViewerComponent],
template: `
<fb-form-viewer
[schema]="formSchema()"
[initialValues]="savedValues()"
[submitLabel]="'Submit Survey'"
[showCancel]="true"
[cancelLabel]="'Discard'"
[isSubmitting]="saving()"
(submitted)="onSubmit($event)"
(saveDraft)="onSaveDraft($event)"
(cancelled)="onCancel()"
/>
`
})
export class MySurveyPage {
readonly formSchema = signal<IFormSchema>(/* loaded schema */);
readonly savedValues = signal<Record<string, unknown>>({});
readonly saving = signal(false);
onSubmit(values: Record<string, unknown>): void {
this.saving.set(true);
this.http.post('/api/submissions', values).subscribe(() => this.saving.set(false));
}
onSaveDraft(values: Record<string, unknown>): void {
sessionStorage.setItem('draft', JSON.stringify(values));
}
onCancel(): void {
this.router.navigate(['/']);
}
}Features:
- Section-based navigation with steps (Next button, no Previous)
- Progress bar (configurable via schema settings)
- Real-time validation with error display
- Conditional logic evaluation (hide fields when conditions met)
- Draft saving support
- Cancel button support (optional)
- Disabled/readonly mode
- Responsive layout with mobile-optimized actions
FormResultViewerComponent
Display submitted answers in a readable format.
Selector: fb-form-result-viewer
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| schema | IFormSchema | required | Form schema |
| result | IFormSubmission | required | Submission data |
| showAnalytics | boolean | false | Show completion stats |
| Output | Type | Description |
|--------|------|-------------|
| fieldClick | { field, value } | Emits when a field is clicked |
import { FormResultViewerComponent } from '@flusys/ng-form-builder';
@Component({
imports: [FormResultViewerComponent],
template: `
<fb-form-result-viewer
[schema]="formSchema()"
[result]="submission()"
[showAnalytics]="true"
/>
`
})
export class SubmissionViewPage {
readonly formSchema = signal<IFormSchema>(/* schema */);
readonly submission = signal<IFormSubmission>(/* submission data */);
}Features:
- Accordion-based section display (section mode) or flat list (flat mode)
- ID-to-label mapping for all option fields
- Formatted display by field type (stars for rating, labels for likert, etc.)
- Analytics: total fields, answered count, completion percentage
- Draft/Completed status badge
- Computed fields display with purple-themed styling
- Responsive layout with mobile-optimized padding and typography
- Dark mode support via CSS variables
Field Types
| Type | Component | Category | Features |
|------|-----------|----------|----------|
| text | TextFieldComponent | input | Single/multi-line, placeholder, min/maxLength |
| number | NumberFieldComponent | input | Min/max, step, prefix/suffix, buttons |
| email | EmailFieldComponent | input | Email validation, custom pattern support |
| checkbox | CheckboxFieldComponent | selection | Boolean toggle, custom label |
| radio | RadioFieldComponent | selection | Single selection, vertical/horizontal layout |
| dropdown | DropdownFieldComponent | selection | Searchable, showClear option |
| multi_select | MultiSelectFieldComponent | selection | Multiple selection, chip/comma display |
| date | DateFieldComponent | specialized | Single/range/multiple, optional time, date format |
| likert | LikertFieldComponent | specialized | Matrix with configurable rows and scale |
| rating | RatingFieldComponent | specialized | Stars, half-star support, min/max labels |
| file_upload | FileUploadFieldComponent | specialized | Accept types, max files/size, multiple |
ID-Based Options
All selection fields (radio, dropdown, multi_select) use ID-based options:
const field: IRadioField = {
id: 'q1',
type: FieldType.RADIO,
label: 'Are you satisfied?',
options: [
{ id: 'yes', label: 'Yes', order: 0 },
{ id: 'no', label: 'No', order: 1 },
{ id: 'maybe', label: 'Maybe', order: 2 }
]
};
// Submission value: { q1: 'yes' } (ID, not label)Field Width
Fields support width control within layouts:
| Width | Description |
|-------|-------------|
| undefined | Auto (1 Column) - Default, spans single grid column |
| full | Full width (spans all columns) |
| half | Half width (spans ~50% of columns) |
| third | One third width (spans ~33% of columns) |
| quarter | One quarter width (spans ~25% of columns) |
Note: Field width is relative to the section's grid columns. For example, in a 4-column grid:
full= spans 4 columnshalf= spans 2 columnsthird= spans 2 columns (rounded up from 1.33)quarter= spans 1 column
JSON Schema Structure
Complete Example
{
"id": "survey-001",
"version": "1.0.0",
"name": "Customer Feedback Survey",
"description": "Please share your experience",
"settings": {
"allowSaveDraft": true,
"showProgressBar": true,
"showSectionNav": true,
"submitButtonText": "Submit Survey"
},
"sections": [
{
"id": "sec_1",
"name": "Personal Information",
"description": "Basic details",
"order": 0,
"layout": {
"type": "grid",
"config": {
"columns": 2,
"gap": "1rem",
"responsive": true,
"responsiveColumns": { "xs": 1, "sm": 2, "md": 2, "lg": 2, "xl": 2 }
}
},
"fields": [
{
"id": "q1",
"type": "text",
"name": "fullName",
"label": "Full Name",
"placeholder": "Enter your name",
"required": true,
"order": 0,
"width": "full",
"validation": {
"rules": [
{ "type": "required", "message": "Name is required" },
{ "type": "min_length", "value": 2, "message": "Min 2 characters" }
]
}
},
{
"id": "q2",
"type": "email",
"name": "email",
"label": "Email Address",
"required": true,
"order": 1,
"width": "half"
}
]
},
{
"id": "sec_2",
"name": "Feedback",
"order": 1,
"layout": {
"type": "flex",
"config": { "direction": "column", "gap": "1rem", "wrap": true }
},
"fields": [
{
"id": "q3",
"type": "rating",
"name": "satisfaction",
"label": "Overall Satisfaction",
"maxRating": 5,
"required": true,
"order": 0
},
{
"id": "q4",
"type": "radio",
"name": "recommend",
"label": "Would you recommend us?",
"displayLayout": "horizontal",
"options": [
{ "id": "yes", "label": "Yes", "order": 0 },
{ "id": "no", "label": "No", "order": 1 }
],
"order": 1
},
{
"id": "q5",
"type": "text",
"name": "reason",
"label": "Please tell us why",
"rows": 3,
"order": 2,
"logicRule": {
"action": "hide",
"logic": {
"operator": "AND",
"conditions": [
{ "fieldId": "q4", "comparison": "not_equals", "value": "no" }
]
}
}
}
]
}
]
}Form Settings (IFormSettings)
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| allowSaveDraft | boolean | true | Show save draft button |
| showProgressBar | boolean | true | Show progress bar |
| showSectionNav | boolean | true | Show section step navigation |
| shuffleQuestions | boolean | false | Randomize question order |
| submitButtonText | string | 'Submit' | Submit button label |
| useSections | boolean | true | Enable section-based layout (false = flat form) |
| responseMode | 'single' \| 'multiple' | 'multiple' | Control submission limits per user |
| computedFields | IComputedField[] | [] | Fields calculated from form responses on submit |
Response Mode
Controls whether users can submit multiple responses to a form:
| Mode | Description |
|------|-------------|
| multiple | Default. Users can submit unlimited responses |
| single | Each user can only submit once |
Single Response Mode Tracking by Access Type:
| Access Type | Tracking Method | Reliability | Bypass Scenarios |
|-------------|-----------------|-------------|------------------|
| authenticated | Server-side via userId | Reliable | None (requires new account) |
| action_group | Server-side via userId | Reliable | None (requires new account) |
| public | Client-side localStorage | Best-effort | Clear browser data, incognito mode, different browser/device |
Implementation Details:
- Authenticated/Action Group forms: Backend checks
FormResulttable for existing non-draft submissions bysubmittedById. ThehasUserSubmittedAPI endpoint returns a boolean. - Public forms: Frontend stores submission flag in
localStoragewith keyform-submitted-{formId}. This is checked before showing the form.
Public Form Limitations (By Design):
Public forms have no reliable way to track anonymous users. The localStorage approach provides a "best-effort" prevention of duplicate submissions but can be bypassed by:
- Clearing browser data or using incognito/private mode
- Using a different browser or device
- Disabling JavaScript localStorage
This is expected behavior - for strict single-response enforcement, use authenticated or action_group access type instead.
Section Mode vs Flat Mode
The form builder supports two layout modes:
Section Mode (useSections: true) - Default
- Form is organized into sections
- Each section contains fields
- Viewer shows step-based navigation between sections
- Progress bar and section navigation visible
Flat Mode (useSections: false)
- Form has no visible sections
- Fields are displayed directly on canvas
- Viewer shows all fields in a single view
- No section navigation or progress bar
- Internally uses a hidden default section (
DEFAULT_SECTION_ID)
Toggle between modes using the toolbar switch in the builder.
Computed Fields
Computed fields are values calculated from form responses when the form is submitted. They support conditional logic, arithmetic operations, and field references.
Structure
interface IComputedField {
id: string; // Unique identifier
name: string; // Display name
key: string; // Key in submission data (_computed.key)
valueType: 'string' | 'number';
rules: IComputedRule[]; // Evaluated in order, first match wins
defaultValue?: string | number | null;
}
interface IComputedRule {
id: string;
condition?: IComputedConditionGroup; // Optional - if omitted, always applies
computation: IComputation;
}Computation Types
| Type | Description | Example |
|------|-------------|---------|
| direct | Set a static value | Score = 100 |
| field_reference | Copy value from another field | Result = field_x value |
| arithmetic | Calculate from fields/constants | Total = field_a + field_b |
Arithmetic Operations
| Operation | Description | Example |
|-----------|-------------|---------|
| sum | Add all operands | a + b + c |
| subtract | Subtract from first | a - b - c |
| multiply | Multiply all | a × b × c |
| divide | Divide sequentially | a ÷ b ÷ c |
| average | Average of operands | (a + b + c) / 3 |
| min | Minimum value | min(a, b, c) |
| max | Maximum value | max(a, b, c) |
| increment | Add to base | a + b + c (same as sum) |
| decrement | Subtract from base | a - b - c (same as subtract) |
Example: Conditional Score Calculation
{
"settings": {
"computedFields": [
{
"id": "cf1",
"name": "Total Score",
"key": "total_score",
"valueType": "number",
"defaultValue": 0,
"rules": [
{
"id": "r1",
"condition": {
"operator": "AND",
"conditions": [
{ "fieldId": "rating", "comparison": "greater_or_equal", "value": 4 }
]
},
"computation": {
"type": "arithmetic",
"config": {
"type": "arithmetic",
"operation": "sum",
"operands": [
{ "type": "field", "fieldId": "score1" },
{ "type": "field", "fieldId": "score2" },
{ "type": "constant", "value": 10 }
]
}
}
},
{
"id": "r2",
"computation": {
"type": "direct",
"config": { "type": "direct", "value": 0 }
}
}
]
}
]
}
}Storage
Computed values are stored in the submission data under _computed namespace:
{
"field1": "user input",
"field2": 5,
"_computed": {
"total_score": 15,
"category": "premium"
}
}UI Configuration
Use the ComputedFieldsEditorComponent in the Settings tab to configure computed fields visually:
import { ComputedFieldsEditorComponent } from '@flusys/ng-form-builder';
@Component({
imports: [ComputedFieldsEditorComponent],
template: `
<fb-computed-fields-editor
[sections]="formSchema.sections"
[computedFields]="formSchema.settings?.computedFields || []"
(computedFieldsChange)="onComputedFieldsChange($event)"
/>
`
})Display Interface
For displaying computed field values in custom result viewers:
import { IComputedFieldDisplay } from '@flusys/ng-form-builder';
// Used by FormResultViewerComponent to display computed values
interface IComputedFieldDisplay {
name: string; // Display name
key: string; // Key in _computed namespace
value: string | number | null;
valueType: 'string' | 'number'; // For formatting
}The FormResultViewerComponent automatically displays computed fields from the _computed namespace with appropriate styling (purple theme, monospace numbers).
Conditional Logic
Rule Structure
Fields are visible by default. Use logic rules to hide or require fields conditionally.
interface IConditionalRule {
action: LogicAction; // 'hide' | 'require' | 'jump'
logic: ILogicGroup;
targetId?: string; // For section-level rules targeting fields or sections
targetType?: 'field' | 'section';
}
interface ILogicGroup {
operator: LogicOperator; // 'AND' | 'OR'
conditions: Array<ICondition | ILogicGroup>; // Supports nesting
}
interface ICondition {
fieldId: string;
comparison: ComparisonOperator;
value: unknown;
}Logic Actions
| Action | Level | Description |
|--------|-------|-------------|
| hide | Field/Section | Hide field when conditions are met (default: visible) |
| require | Field/Section | Make field required when conditions are met |
| jump | Section only | Jump to target section when conditions are met |
Comparison Operators
| Operator | Description |
|----------|-------------|
| equals | Value equals target |
| not_equals | Value does not equal target |
| contains | String/array contains value |
| not_contains | String/array does not contain value |
| greater_than | Number greater than |
| less_than | Number less than |
| greater_or_equal | Number greater than or equal |
| less_or_equal | Number less than or equal |
| is_empty | Value is null/undefined/empty |
| is_not_empty | Value has content |
| in | Value is in array |
| not_in | Value is not in array |
Example: Hide Field Conditionally
Hide a field unless certain conditions are met (inverse logic since default is visible):
{
"id": "reason_field",
"type": "text",
"label": "Please explain",
"logicRule": {
"action": "hide",
"logic": {
"operator": "OR",
"conditions": [
{ "fieldId": "q1", "comparison": "not_equals", "value": "no" },
{ "fieldId": "q2", "comparison": "greater_or_equal", "value": 3 }
]
}
}
}This hides the field when q1 is NOT "no" OR q2 is >= 3 (showing only when q1="no" AND q2 < 3).
Validation Rules
| Type | Value | Description |
|------|-------|-------------|
| required | - | Field must have a value |
| min_length | number | Minimum string length |
| max_length | number | Maximum string length |
| min | number | Minimum numeric value |
| max | number | Maximum numeric value |
| pattern | string | Regex pattern |
| email | - | Valid email format |
| custom | string | Custom validator name |
{
"validation": {
"rules": [
{ "type": "required", "message": "This field is required" },
{ "type": "min_length", "value": 5, "message": "Min 5 characters" },
{ "type": "pattern", "value": "^[A-Z].*", "message": "Must start with uppercase" }
]
}
}Email Pattern Validation
Email fields support custom regex patterns for domain restrictions:
{
"id": "work_email",
"type": "email",
"label": "Work Email",
"pattern": "@company\\.com$",
"patternMessage": "Please use your company email"
}Common patterns:
@company\\.com$- Single domain@(company1|company2)\\.com$- Multiple domains@.*\\.gov$- Government emails
Custom Validators
import { ValidationService } from '@flusys/ng-form-builder';
// Register custom validator
validationService.registerCustomValidator('phone', (value, field) => {
if (typeof value !== 'string') return 'Invalid phone';
const phoneRegex = /^\+?[\d\s-]{10,}$/;
return phoneRegex.test(value) ? null : 'Invalid phone number';
});
// Use in schema
{
"validation": {
"rules": [
{ "type": "custom", "value": "phone" }
]
}
}Section Layouts
Flex Layout
{
"layout": {
"type": "flex",
"config": {
"direction": "column",
"wrap": true,
"gap": "1rem",
"alignItems": "stretch",
"justifyContent": "flex-start"
}
}
}Grid Layout
{
"layout": {
"type": "grid",
"config": {
"columns": 2,
"gap": "1rem",
"rowGap": "1.5rem",
"columnGap": "1rem",
"responsive": true,
"responsiveColumns": { "xs": 1, "sm": 2, "md": 2, "lg": 2, "xl": 2 }
}
}
}Responsive Grid Layout
The grid layout supports automatic responsive column adjustment based on screen size. This ensures forms look good on all devices from mobile to desktop.
Responsive Breakpoints
| Breakpoint | Screen Width | Default Behavior |
|------------|--------------|------------------|
| xs | < 576px (mobile) | 1 column |
| sm | 576px - 767px (tablet portrait) | min(2, columns) |
| md | 768px - 991px (tablet landscape) | min(3, columns) |
| lg | 992px - 1199px (desktop) | columns |
| xl | >= 1200px (large desktop) | columns |
Grid Configuration Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| columns | number | 2 | Base/max number of columns |
| gap | string | '1rem' | Gap between all items |
| rowGap | string | - | Override gap for rows only |
| columnGap | string | - | Override gap for columns only |
| responsive | boolean | true | Enable responsive column adjustment |
| responsiveColumns | IResponsiveColumns | auto | Custom column counts per breakpoint |
Custom Responsive Configuration
{
"layout": {
"type": "grid",
"config": {
"columns": 4,
"gap": "1.5rem",
"responsive": true,
"responsiveColumns": {
"xs": 1,
"sm": 2,
"md": 2,
"lg": 3,
"xl": 4
}
}
}
}Disabling Responsive Behavior
Set responsive: false to use fixed columns at all screen sizes:
{
"layout": {
"type": "grid",
"config": {
"columns": 3,
"gap": "1rem",
"responsive": false
}
}
}Field Width with Responsive Grid
Field widths work with responsive grids. The span is calculated relative to the active column count:
| Field Width | 4-col (xl) | 2-col (sm) | 1-col (xs) |
|-------------|------------|------------|------------|
| undefined | span 1 | span 1 | span 1 |
| full | span 4 | span 2 | span 1 |
| half | span 2 | span 1 | span 1 |
| third | span 2 | span 1 | span 1 |
| quarter | span 1 | span 1 | span 1 |
Note: Spans are automatically clamped to the available columns at each breakpoint.
CSS Custom Properties for Responsive Grid
The section viewer uses CSS custom properties to enable responsive grid layouts:
| Property | Description |
|----------|-------------|
| --fb-cols-xs | Columns at < 576px |
| --fb-cols-sm | Columns at 576px - 767px |
| --fb-cols-md | Columns at 768px - 991px |
| --fb-cols-lg | Columns at 992px - 1199px |
| --fb-cols-xl | Columns at >= 1200px |
| --fb-gap | Gap between grid items |
These are set on the host element and used by the .fb-responsive-grid CSS class:
.fb-responsive-grid {
display: grid;
gap: var(--fb-gap, 1rem);
grid-template-columns: repeat(var(--fb-cols-xl, 2), 1fr);
}
@media (max-width: 575px) {
.fb-responsive-grid {
grid-template-columns: repeat(var(--fb-cols-xs, 1), 1fr);
}
}Signal Patterns
The package follows Angular 21 signal best practices:
Private Writable + Public Readonly
// Service pattern
private readonly _registry = signal<Map<string, IFieldTypeMetadata>>(
this.createInitialRegistry() // Initialize with data directly
);
readonly fieldTypes = this._registry.asReadonly();
readonly allFieldTypes = computed(() => Array.from(this._registry().values()));Modern Query Signals
// Use viewChildren() instead of @ViewChildren decorator
private readonly tooltips = viewChildren(Tooltip);
// Access via function call
this.tooltips().forEach(tooltip => tooltip.deactivate());Computed Override Pattern (avoid allowSignalWrites)
// Instead of effect with allowSignalWrites, use computed with override signal
private readonly _collapsedOverride = signal<boolean | null>(null);
readonly collapsed = computed(() => {
const override = this._collapsedOverride();
if (override !== null) return override;
return this.section().isCollapsed ?? false;
});
toggleCollapsed(): void {
this._collapsedOverride.set(!this.collapsed());
}Signal-based UI State
// All component UI state should use signals
readonly activeTab = signal<string>('general');
readonly searchQuery = signal<string>('');
// Template binding with signals
// [value]="activeTab()" (valueChange)="onTabChange($event)"Signal Initialization
Important: Initialize signals with data directly rather than updating in constructors for reliable rendering in zoneless Angular:
// CORRECT - Initialize with data
private readonly _registry = signal(this.createInitialRegistry());
// AVOID - Update in constructor (can cause timing issues)
private readonly _registry = signal(new Map());
constructor() {
this.populateRegistry(); // Signal update may not propagate
}Services
FormBuilderStateService
Manages builder UI state. Provided at component level by FormBuilderComponent.
Key Signals:
schema()/isDirty()selectedSection()/selectedField()/allFields()sections()/formName()/formSettings()
Schema Operations:
loadSchema(schema)/createNewSchema(name)/updateFormMeta(updates)/markAsSaved()
Section Operations:
addSection()/updateSection(id, updates)/deleteSection(id)reorderSections(from, to)/duplicateSection(id)
Field Operations:
addField(sectionId, fieldType)/updateField(sectionId, fieldId, updates)deleteField(sectionId, fieldId)/reorderFields(sectionId, from, to)moveFieldToSection(fromId, toId, fieldId, toIndex)/duplicateField(sectionId, fieldId)
Selection:
selectSection(id)/selectField(id)/clearSelection()
ConditionalLogicService
Evaluates conditional visibility rules. Provided at component level.
import { ConditionalLogicService } from '@flusys/ng-form-builder';
@Component({ providers: [ConditionalLogicService] })
export class MyComponent {
private readonly logicService = inject(ConditionalLogicService);
checkVisibility(field: IField): boolean {
return this.logicService.evaluateFieldVisibility(field);
}
updateValue(fieldId: string, value: unknown): void {
this.logicService.updateFormValue(fieldId, value);
}
}ValidationService
Validates field values and form submissions. Provided at root. Uses immutable internal state with readonly collections.
import { ValidationService } from '@flusys/ng-form-builder';
// Validate single field
const error = validationService.validateField(field, value); // string | null
// Validate section
const errors = validationService.validateSection(section, values); // Map<fieldId, message>
// Validate entire form
const allErrors = validationService.validateForm(sections, values); // IValidationError[]
// Register custom validators
validationService.registerCustomValidator('phone', (value, field) => {
if (typeof value !== 'string') return 'Invalid phone';
return /^\+?[\d\s-]{10,}$/.test(value) ? null : 'Invalid phone number';
});SchemaExportService
Import/export JSON schemas with validation. Provided at root.
import { SchemaExportService } from '@flusys/ng-form-builder';
// Export
schemaExportService.exportToFile(schema, 'my-form.json');
const json = schemaExportService.exportToJson(schema, true); // pretty-printed
// Import from JSON string
const schema = schemaExportService.importFromJson(jsonString);
// Import from File object
const schemaFromFile = await schemaExportService.importFromFile(file);
// Validate
const { valid, errors } = schemaExportService.validateSchema(schema);PdfExportService
Export form submissions to PDF. Provided at root. Requires pdfmake (optional peer dependency).
import { PdfExportService, IPdfExportConfig } from '@flusys/ng-form-builder';
const config: IPdfExportConfig = {
title: 'Survey Results',
subtitle: 'Customer Feedback Q1 2026',
showSummary: true,
primaryColor: '#2563EB',
pageSize: 'A4',
};
// Download PDF file
await pdfService.downloadPdf(schema, submission, config);
// Open in new tab
await pdfService.openPdf(schema, submission, config);
// Get as Blob
const blob = await pdfService.getPdfBlob(schema, submission, config);PDF Configuration (IPdfExportConfig):
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| title | string | Form name | Document title |
| subtitle | string | - | Subtitle text |
| showMetadata | boolean | true | Show submission date/version |
| showDescription | boolean | true | Show form description |
| showSectionDescriptions | boolean | true | Show section descriptions |
| showSummary | boolean | true | Show summary statistics |
| primaryColor | string | #2563EB | Header/accent color (hex) |
| accentColor | string | #10B981 | Secondary color (hex) |
| pageSize | string | A4 | A4, LETTER, LEGAL |
| pageOrientation | string | portrait | portrait or landscape |
| footerText | string | Generated by Form Builder | Footer text |
FieldRegistryService
Extensible field type registry. Provided at root. Built-in types auto-registered at creation time for reliable zoneless rendering.
import { FieldRegistryService } from '@flusys/ng-form-builder';
// Signal-based reactive access
const all = fieldRegistry.allFieldTypes(); // Signal<IFieldTypeMetadata[]>
const inputs = fieldRegistry.inputFieldTypes(); // Computed signal
const selections = fieldRegistry.selectionFieldTypes();
const specialized = fieldRegistry.specializedFieldTypes();
// Method-based access (imperative)
const all = fieldRegistry.getAll();
const inputs = fieldRegistry.getByCategory('input');
// Register custom field type
fieldRegistry.register({
type: 'custom_type',
label: 'Custom Field',
icon: 'pi pi-star',
category: 'specialized',
component: MyCustomFieldComponent,
});Implementation Note: The registry initializes built-in types directly in the signal definition (createInitialRegistry()) rather than in the constructor to ensure data is available synchronously for zoneless Angular.
Utilities & Helpers
Sorting Functions
import { sortByOrder, sortFields, sortSections, sortOptions } from '@flusys/ng-form-builder';
const sorted = sortFields(fields); // Sort by order property
const sorted = sortSections(sections); // Sort by order property
const sorted = sortOptions(options); // Sort by order propertyValue Helpers
import { isEmpty, hasValue, formatFileSize } from '@flusys/ng-form-builder';
isEmpty(null); // true
isEmpty(''); // true
isEmpty([]); // true
isEmpty({}); // true
isEmpty('hello'); // false
hasValue('hello'); // true (inverse of isEmpty)
formatFileSize(1024); // '1 KB'
formatFileSize(1048576); // '1 MB'Factory Functions
import { createFormSchema, createSection } from '@flusys/ng-form-builder';
// Create schema with defaults (generates ID, version, timestamps)
const schema = createFormSchema({ name: 'My Form' });
// Create section with defaults (generates ID, empty fields, default layout)
const section = createSection({ name: 'Section 1' });Type Guards
import { hasOptions, isLogicGroup } from '@flusys/ng-form-builder';
// Check if field has options (radio, dropdown, multi_select)
if (hasOptions(field)) {
console.log(field.options);
}
// Check if condition item is a nested logic group
if (isLogicGroup(item)) {
console.log(item.operator, item.conditions);
}Constants
import {
DEFAULT_FORM_SETTINGS,
DEFAULT_SECTION_LAYOUT,
DEFAULT_FLEX_LAYOUT,
DEFAULT_GRID_LAYOUT,
FIELD_TYPE_INFO,
createResponsiveColumns, // Helper to create responsive config
} from '@flusys/ng-form-builder';
// Create responsive columns config based on max columns
const responsiveCols = createResponsiveColumns(4);
// Result: { xs: 1, sm: 2, md: 3, lg: 4, xl: 4 }Styling & Theming
CSS Classes
| Class | Description |
|-------|-------------|
| .fb-form-builder | Main builder container |
| .fb-field | Field wrapper |
| .fb-field-label | Field label |
| .fb-help-text | Help text below field |
| .fb-error | Error message with icon |
| .fb-required | Required indicator (*) |
| .fb-section | Section container |
| .fb-responsive-grid | Responsive grid layout |
| .fb-section-panel | Section panel wrapper |
Dark Mode Support
All components use CSS variables for theming:
| Variable | Usage |
|----------|-------|
| --text-color | Primary text |
| --text-color-secondary | Help text, muted text |
| --surface-ground | Background surfaces |
| --surface-card | Card backgrounds |
| --surface-border | Border colors |
| --primary-color | Primary accent |
| --primary-100 to --primary-700 | Primary color shades |
| --p-red-500 | Error states |
| --highlight-bg | Highlighted backgrounds |
Scrollable Tabs
All tabs use [scrollable]="true" with horizontal scroll CSS:
:host ::ng-deep .p-tablist {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
:host ::ng-deep .p-tablist-content {
flex-wrap: nowrap;
}Custom Styling
.fb-field {
margin-bottom: 1.5rem;
&.has-error {
.fb-field-label {
color: var(--p-red-500);
}
}
}
.fb-section {
background: var(--surface-card);
border-radius: var(--border-radius);
padding: 1.5rem;
}
/* Responsive padding */
@media (max-width: 639px) {
.fb-section {
padding: 0.75rem;
}
}Best Practices
Schema Design
Do:
- Use ID-based options for all selection fields
- Add meaningful validation messages
- Use conditional logic to simplify complex forms
- Export schemas for backup/versioning
- Use sections to organize long forms
- Set appropriate field widths for better layouts
- Enable responsive grid layout for mobile-friendly forms
- Use "Auto" (undefined) field width for standard single-column fields
Don't:
- Don't use labels as option values (use IDs)
- Don't create circular conditional logic dependencies
- Don't mix validation in UI code (use schema validation rules)
- Don't modify schema structure outside the builder service
- Don't disable responsive layout unless you have a specific reason
Signal Patterns
Do:
- Initialize signals with data directly (not in constructors)
- Use
viewChildren()instead of@ViewChildrendecorator - Use computed override pattern instead of
effectwithallowSignalWrites - Mark all service collections with
readonlymodifier - Use
input(),output()instead of decorators - Split two-way bindings:
[value]="signal()"+(valueChange)="onHandler($event)"
Don't:
- Don't update signals in constructors (causes zoneless timing issues)
- Don't use
allowSignalWrites: truein effects (use computed pattern instead) - Don't use
@ViewChildren/@ViewChilddecorators (use signal queries) - Don't use
[(ngModel)]with signals (split the binding)
Responsive Design
Do:
- Use CSS variables for colors (dark mode support)
- Add
[scrollable]="true"top-tabsfor horizontal scrolling on mobile - Use
@media (min-width: 640px)breakpoint for responsive padding/font sizes - Use
[paginator]="total() > 0"to hide pagination when no data - Use Tailwind responsive classes (
flex-col sm:flex-row) where appropriate
Don't:
- Don't use hardcoded hex colors (use CSS variables instead)
- Don't use fixed widths without responsive alternatives
- Don't add vertical scrolling to tab lists (use horizontal scroll)
Pages and Routes
The package provides ready-to-use pages for form management with backend API integration.
Admin Routes (Authenticated)
import { FORM_BUILDER_ADMIN_ROUTES } from '@flusys/ng-form-builder';
// In your app routes (requires authentication)
{
path: 'forms/manage',
loadChildren: () => import('@flusys/ng-form-builder').then(m => m.FORM_BUILDER_ADMIN_ROUTES)
}Public Routes (Optional Authentication)
import { FORM_BUILDER_PUBLIC_ROUTES } from '@flusys/ng-form-builder';
// In your app routes (public access)
{
path: 'forms/public',
loadChildren: () => import('@flusys/ng-form-builder').then(m => m.FORM_BUILDER_PUBLIC_ROUTES)
}Available Pages
| Page | Route | Description |
|------|-------|-------------|
| Form List | /forms/manage | List all forms with CRUD operations |
| Form Details | /forms/manage/:id | Form builder, settings, and results |
| Form Details (New) | /forms/manage/new | Create new form |
| Result Viewer | /forms/manage/:id/results/:resultId | View single submission |
| Public Form | /forms/public/:id | Public form submission page |
API Services
| Service | Description |
|---------|-------------|
| FormApiService | Form CRUD, access info, public/authenticated form endpoints |
| FormResultApiService | Submit forms, get results by form ID |
Build
# Build the package
cd FLUSYS_NG && npm run build:ng-form-builder
# Build all libs (includes ng-form-builder)
cd FLUSYS_NG && npm run build:libsSee Also
- NestJS Form Builder Guide - Backend API reference
- Angular Signals Guide
- Angular Components Guide
- Shared Guide - Provider interfaces
Last Updated: 2026-02-18 Angular Version: 21
