npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/pdfmake

Components

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 columns
  • half = spans 2 columns
  • third = 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 FormResult table for existing non-draft submissions by submittedById. The hasUserSubmitted API endpoint returns a boolean.
  • Public forms: Frontend stores submission flag in localStorage with key form-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 property

Value 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 @ViewChildren decorator
  • Use computed override pattern instead of effect with allowSignalWrites
  • Mark all service collections with readonly modifier
  • 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: true in effects (use computed pattern instead)
  • Don't use @ViewChildren/@ViewChild decorators (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" to p-tabs for 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:libs

See Also


Last Updated: 2026-02-18 Angular Version: 21