Events
The events API provides two complementary services for working with form events. Which one you use depends on where your code lives relative to the element.
Choosing the right API
| | |
|---|---|---|
| Where to use | Inside DynamicForm (field components) | Outside DynamicForm (host components) |
| How to get it | inject( | providers: [ + inject( |
| Dispatches | Constructor + args | Pre-built event instances |
| Subscribes | Yes — .on() method | No |
Important:
is scoped to theEventBus component's injector tree. It is only available to components rendered inside DynamicForm (i.e. custom field components). If you injectDynamicForm in a parent or host component, you get a completely separate, disconnected instance that the form knows nothing about. UseEventBus instead.EventDispatcher
EventDispatcher — dispatching from outside DynamicForm
Use when you need to drive form behaviour from the host component — for example, appending array items in response to a field value change, triggering a form reset from a toolbar button, or reacting to external application state.
Setup
Provide at the host component level (not root). automatically detects it and connects its internal event bus.
import { Component, effect, inject, signal } from '@angular/core';
import { DynamicForm , EventDispatcher , FormConfig , arrayEvent } from '@ng-forge/dynamic-forms';
@Component({
providers: [EventDispatcher ], // Provide at host component level
imports: [DynamicForm ],
template: `<form [dynamic-form]="config" [(value)]="formValue"></form>`,
})
export class MyFormComponent {
protected readonly config = { fields: [...] } as const satisfies FormConfig ;
readonly formValue = signal<Record<string, unknown>>({});
private readonly dispatcher = inject(EventDispatcher );
constructor() {
effect(() => {
const category = this.formValue()?.['category'] as string | undefined;
if (category) {
this.dispatcher.dispatch(
arrayEvent ('tasks').append([{ key: 'name', type: 'input', label: 'Task', value: category }])
);
}
});
}
}Dispatching events
accepts any instance. Use the builder for array operations:
// Array manipulation
this.dispatcher.dispatch(arrayEvent ('contacts').append(contactTemplate));
this.dispatcher.dispatch(arrayEvent ('contacts').removeAt(0));
this.dispatcher.dispatch(arrayEvent ('contacts').pop());
// Form lifecycle
this.dispatcher.dispatch(new FormResetEvent ());
this.dispatcher.dispatch(new FormClearEvent ());
// Custom events
this.dispatcher.dispatch(new MyCustomEvent());Multi-form note
If multiple instances exist under the same provider scope, all forms receive dispatched events. To target a specific form, scope the provider to a wrapper component that contains only that form.
Listening to events from outside DynamicForm
For observing events from a host component, use the output bindings exposed directly on the element. This avoids any DI scoping concerns and works with standard Angular event binding syntax.
Output bindings
| Output | Emits |
|---|---|
(events) | Every form event (full stream) |
(submitted) | Form value when submitted and valid (SubmitEvent) |
(reset) | When the form is reset to default values |
(cleared) | When the form is cleared to empty state |
(onPageChange) | PageChangeEvent on each wizard page navigation |
(onPageNavigationStateChange) | Navigation state changes (canGoNext, canGoPrevious, etc.) |
(validityChange) | Boolean — whenever form validity changes |
(dirtyChange) | Boolean — whenever form dirty state changes |
(initialized) | Once all field components are rendered and ready |
Examples
React to specific well-known events via dedicated outputs:
@Component({
imports: [DynamicForm ],
template: `
<form [dynamic-form]="config" (submitted)="onSubmit($event)" (reset)="onReset()" (onPageChange)="onPageChange($event)"></form>
`,
})
export class MyFormComponent {
onSubmit(value: Record<string, unknown>) {
console.log('Valid submission:', value);
}
onReset() {
console.log('Form reset to defaults');
}
onPageChange(event: PageChangeEvent ) {
console.log(`Now on page ${event.currentPageIndex + 1} of ${event.totalPages}`);
}
}React to custom or less common events via (events):
@Component({
imports: [DynamicForm ],
template: `<form [dynamic-form]="config" (events)="onEvent($event)"></form>`,
})
export class MyFormComponent {
onEvent(event: FormEvent ) {
if (event.type === 'save-draft') {
this.saveDraft();
}
}
}Note:
(submitted)only fires when the form is valid. To handle submit events regardless of validity, use(events)and filter forevent.type === 'submit'.
EventBus — dispatching from inside DynamicForm
is the internal event bus scoped to each instance. Inject it inside custom field components to communicate with the parent form or other fields within the same form.
Scoping reminder:
is provided byEventBus via its component injector. It is only resolvable from within field components rendered by that form. Do not inject it in host or parent components — you will get a disconnected, standalone instance.DynamicForm
Usage in custom field components
import { Component, inject } from '@angular/core';
import { EventBus , SubmitEvent } from '@ng-forge/dynamic-forms';
@Component({
selector: 'app-custom-submit-button',
template: `<button (click)="submit()">Submit Form</button>`,
})
export class CustomSubmitButton {
private readonly eventBus = inject(EventBus );
submit() {
this.eventBus.dispatch(SubmitEvent );
}
}Subscribing to events
export class CustomFieldComponent {
private readonly eventBus = inject(EventBus );
ngOnInit() {
this.eventBus.on<PageChangeEvent >('page-change').subscribe((event) => {
console.log(`Navigated to page ${event.currentPageIndex + 1}`);
});
}
}Built-in Events
SubmitEvent
Fired when form is submitted.
eventBus.on<SubmitEvent >('submit').subscribe(() => {
console.log('Form submitted');
});PageChangeEvent
Fired when navigating between wizard pages.
eventBus.on<PageChangeEvent >('page-change').subscribe((event) => {
console.log(`Page ${event.currentPageIndex + 1} of ${event.totalPages}`);
console.log(`Previous page: ${event.previousPageIndex}`);
});Properties:
currentPageIndex: number- Current page (0-based)totalPages: number- Total number of pagespreviousPageIndex?: number- Previous page index
NextPageEvent
Navigate to next page in wizard.
eventBus.dispatch(NextPageEvent );PreviousPageEvent
Navigate to previous page in wizard.
eventBus.dispatch(PreviousPageEvent );FormResetEvent
Reset form to default values.
// Dispatch reset event
eventBus.dispatch(FormResetEvent );
// Listen for reset
eventBus.on<FormResetEvent >('form-reset').subscribe(() => {
console.log('Form was reset to defaults');
});FormClearEvent
Clear all form values (empty state, not defaults).
// Dispatch clear event
eventBus.dispatch(FormClearEvent );
// Listen for clear
eventBus.on<FormClearEvent >('form-clear').subscribe(() => {
console.log('Form was cleared');
});Array Events
The builder provides a fluent API for array field manipulation. Import it from the main package:
import { arrayEvent } from '@ng-forge/dynamic-forms';Adding Items
Important: A template is required for all add operations. The template defines the structure of the new item:
- Single FieldDef → creates a primitive item (field value extracted directly)
- Array of FieldDefs → creates an object item (fields merged into object)
// Define templates
const tagTemplate = { key: 'tag', type: 'input', label: 'Tag' }; // Primitive item
const contactTemplate = [
// Object item
{ key: 'name', type: 'input', label: 'Name' },
{ key: 'email', type: 'input', label: 'Email' },
];
// Append item at end (most common)
eventBus.dispatch(arrayEvent ('tags').append(tagTemplate));
eventBus.dispatch(arrayEvent ('contacts').append(contactTemplate));
// Prepend item at beginning
eventBus.dispatch(arrayEvent ('contacts').prepend(contactTemplate));
// Insert at specific index
eventBus.dispatch(arrayEvent ('contacts').insertAt(2, contactTemplate));Removing Items
// Remove last item (stack pop)
eventBus.dispatch(arrayEvent ('contacts').pop());
// Remove first item (queue shift)
eventBus.dispatch(arrayEvent ('contacts').shift());
// Remove item at specific index
eventBus.dispatch(arrayEvent ('contacts').removeAt(2));Internal Event Classes
For advanced use cases (extending events or type-checking), you can import the underlying event classes:
import {
AppendArrayItemEvent ,
PrependArrayItemEvent ,
InsertArrayItemEvent ,
PopArrayItemEvent ,
ShiftArrayItemEvent ,
RemoveAtIndexEvent ,
} from '@ng-forge/dynamic-forms';
// Example: extend for a pre-filled template
export class AddContactEvent extends AppendArrayItemEvent {
constructor() {
super('contacts', [
{ key: 'name', type: 'input', label: 'Name' },
{ key: 'phone', type: 'input', label: 'Phone' },
]);
}
}
// Usage with EventDispatcher (from outside)
dispatcher.dispatch(new AddContactEvent());
// Usage with EventBus (from inside a field component)
eventBus.dispatch(AddContactEvent);Event types:
| Event | Description |
|---|---|
| Add item at end of array |
| Add item at beginning of array |
| Add item at specific index |
| Remove last item |
| Remove first item |
| Remove item at specific index |
Multiple Event Subscriptions
Subscribe to multiple event types by passing an array of type strings:
eventBus.on<SubmitEvent | PageChangeEvent | NextPageEvent >(['submit', 'page-change', 'next-page']).subscribe((event) => {
switch (event.type) {
case 'submit':
handleSubmit();
break;
case 'page-change':
handlePageChange(event);
break;
case 'next-page':
handleNextPage();
break;
}
});Custom Events
Create custom events for your forms:
import { FormEvent } from '@ng-forge/dynamic-forms';
// Simple event
export class SaveDraftEvent implements FormEvent {
readonly type = 'save-draft' as const;
}
// Event with payload
export class ValidationErrorEvent implements FormEvent {
readonly type = 'validation-error' as const;
constructor(
public readonly fieldKey: string,
public readonly errorMessage: string,
) {}
}Usage inside a field component (via ):
// Dispatch
eventBus.dispatch(SaveDraftEvent);
eventBus.dispatch(ValidationErrorEvent, 'email', 'Invalid email format');
// Subscribe
eventBus.on<SaveDraftEvent>('save-draft').subscribe(() => saveDraft());
eventBus.on<ValidationErrorEvent>('validation-error').subscribe((event) => {
showError(event.fieldKey, event.errorMessage);
});Usage from a host component (via ):
// Dispatch a pre-built instance
dispatcher.dispatch(new SaveDraftEvent());
dispatcher.dispatch(new ValidationErrorEvent('email', 'Invalid email format'));Attaching Form Values to Events
By default, events don't include the form's current value. You can opt-in to automatically attach the form value to all dispatched events using .
Global Opt-in
Enable for all forms in your application:
import { provideDynamicForm , withEventFormValue } from '@ng-forge/dynamic-forms';
provideDynamicForm (...withMaterialFields (), withEventFormValue ());Per-Form Control
Override the global setting for specific forms:
const config = {
fields: [...],
options: {
// Enable for this form (even if globally disabled)
emitFormValueOnEvents: true,
// Or disable for this form (even if globally enabled)
// emitFormValueOnEvents: false,
},
} as const satisfies FormConfig ;Accessing the Form Value
Use the type guard to safely access the attached value:
import { hasFormValue } from '@ng-forge/dynamic-forms';
eventBus.on<SubmitEvent >('submit').subscribe((event) => {
if (hasFormValue (event)) {
// TypeScript knows event.formValue exists
console.log('Form value at submission:', event.formValue);
sendToApi(event.formValue);
}
});This is useful when you need the complete form state at the time an event occurred, such as:
- Logging form values on submission
- Auto-saving on page changes
- Analytics tracking