https://github.com/TheNordicOne/ngx-formwork
https://github.com/TheNordicOne/ngx-formwork
Last synced: about 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/TheNordicOne/ngx-formwork
- Owner: TheNordicOne
- License: mit
- Created: 2025-01-18T13:07:15.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2025-04-20T10:13:11.000Z (2 months ago)
- Last Synced: 2025-04-20T11:25:07.263Z (2 months ago)
- Language: TypeScript
- Size: 636 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-angular - ngx-formwork - This package provides a framework for creating Angular Reactive Forms, based on a configuration. This configuration can come from a server in the form of JSON or directly from an object written in TypeScript. (Table of contents / Third Party Components)
- fucking-awesome-angular - ngx-formwork - This package provides a framework for creating Angular Reactive Forms, based on a configuration. This configuration can come from a server in the form of JSON or directly from an object written in TypeScript. (Table of contents / Third Party Components)
- trackawesomelist - ngx-formwork (⭐3) - This package provides a framework for creating Angular Reactive Forms, based on a configuration. This configuration can come from a server in the form of JSON or directly from an object written in TypeScript. (Recently Updated / [Apr 28, 2025](/content/2025/04/28/README.md))
README
# Formwork

[](https://github.com/TheNordicOne/ngx-formwork/actions/workflows/lint-and-test.yml)
A highly flexible framework for generating declarative reactive forms, based on a configuration.
This package provides a framework for creating Angular Reactive Forms, based on a configuration. This configuration can come from a server in the form of JSON or directly from an object written in TypeScript. It is as close to Angular as possible, to give you the most flexibility, while still taking care of the heavy lifting.
## What is ngx-formwork?
__ngx-formwork__ generates reactive forms from a JSON or a normal object. It supports dynamically hiding, disabling and marking as readonly with complex expressions.
The first key focus is on compatibility with JSON, so that the whole form configuration can actually be stored by any means that are compatible with JSON (either by directly saving "as is" or by serializing).
The second key focus is to be as close to Angular as possible. Not just the whole wording, but also in terms of technologies used. YOu still have full control over the form and use it pretty much like any other form. No additional API to know, just to access the forms value.
### What is the motivation?
There already are at least two popular solutions out there, that pretty much already provide you with "JSON forms". And I think overall they are fine. However. it has been more than enough times, where the requirements of the project made it practically impossible to use any of those. On one projects we needed much more complex conditions for showing and hiding, while also maintaining them in a database. This often was a hard requirement in projects I worked on.
Not even a year ago, when something like this came up again, and I once again checked if those packages maybe were a perfect fit this time. Unfortunately it was again not flexible enough. Though this time the application was already pretty much fully implemented in Angular, so we couldn't just switch to some other tech. Especially because we also needed a UI to configure a form. I ended up implementing a custom soultion with only the bits and pieces actually needed and it worked well.
After the project finished I decided to take the basic idea and turn it into its own package. And here we are.
### What are the key differences compared to other solutions?
- Full JSON compatibility (everything can be defined with primitives)
- Create your own controls, instead of relying on other packages to render your controls
- Full control and access of the form itself
- Decent type safety when writing a form in TS
- Implemented with almost only Angular features
- Expression syntax is essentially JS, only evaluated against the forms value
- Generating a test id for each controlThe two most important features are the JSON compatibility and the fact that you have full control over a controls template and additional behaviors. You can make a control as specialized and as generic as you want.
### Who is this for?
This is for everyone that needs a whole lot of flexibility and control when working with dynamic forms. This is by no means supposed to be a replacement for other solutions and it likely is not a good fit for every projects (or developer).
Setting things up is (probably) about as complex as with other solutions, but of course it requires a little more work to create the components that can actually be used with __ngx-formwork__.
## Current State
> [!WARNING]
> This project is still in the making and not ready to be used in production!## Compatibility
At this time this package is only compatible with Angular 19.2.1 or above.
## Feature Overview
- [x] All form configurations are fully compatible with JSON
- [x] Configurations can easily be stored in a database
- [x] Create your own form controls
- [x] No extra packages for rendering with UI Library X or CSS Framework Y
- [x] Controls are fully typed
- [x] Add whatever options you need for your controls
- [x] Automatic e2e-Id generation
- [x] Hide controls based on an expression and the form state
- [x] Decide how the value of hidden controls behave (keep last, reset, default)
- [x] Disable controls based on an expression and the form state or set it statically
- [x] Compute a controls readonly based on an expression and the form state or set it statically
- [x] Mark a control as required or add other validators
- [x] Combine commonly used validator combinations into a single key
- [x] Expression syntax is like JavaScript
- [x] Expressions only run against the form value
- [ ] Support for non-control blocks (pure informational components like a callout, paragraphs, etc.)
- [ ] Derive a value based on form state (dynamic readonly controls)
- [ ] Support for full TypeScript-only configurations, meaning you can use actual functions for the expressions
- [ ] Dynamic labels based on form state
- [ ] Configuration of test id (attribute name and how value is build)
- [ ] Schematic for adding formwork and scaffolding new controls### Open Feature Ideas
The following are some ideas that are interesting. There is no guarantee that they will be implemented. Let me know if you feel like one of those ideas can be very useful.
- UI for creating valid configurations in JSON
- Optionally allow a form to be handled entirely self-contained## Usage notes
A few things to know about when using _ngx-formwork_
- The `readonly` property itself only provides you with the (dynamic) value. How and if this is handled has to be implemented in the component
- Sometimes when writing a form configuration, TS will throw errors about properties not being compatible. If that happens double check precisely the property names.
- For example: A group can have a title property and a control a label. Adding a label property to a group will confuse TypeScript and it throws Errors about unrelated properties not matching.
- In some cases, when configuring a group in TS, it helps to cast the controls property like so `controls: [...]` or `controls: [...]` if you use the `OneOf` type helper## Getting started
Install _ngx-formwork_
```shell
npm i ngx-formwork
```_ngx-formwork_ is provided and configured in _app.config.ts_.
```ts
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork({
componentRegistrations: {
// Component registrations go here
},
// validatorRegistrations are optional
validatorRegistrations: {
// Validator registrations go here
// Following Angular validators are registered by default
// Validators.required
// Validators.requiredTrue
// Validators.email
// Validators.nullValidator
},
// asyncValidatorRegistrations are optional
asyncValidatorRegistrations: {
// Async Validator registrations go here
},
})
]
};
```To avoid bloating your config, put your registrations in a separate file
**Component Registrations**
```ts
export const componentRegistrations: ComponentRegistrationConfig = {
// Component registrations go here
}
```**Validator Registrations**
```ts
export const validatorRegistrations: ValidatorConfig = {
// Validator registrations go here
}
```**Async Validator Registrations**
```ts
export const asyncValidatorRegistrations: AsyncValidatorConfig = {
// Async Validator registrations go here
}
```**App Config**
```ts
provideFormwork({
componentRegistrations,
validatorRegistrations,
asyncValidatorRegistrations
})
```## How to set up
Registering controls and validators is easy and declarative. Below are examples for creation and registration.
_ngx-formwork_ comes with no pre-built components by design. This gives all flexibility of what framework to use and how to structure the markup. Furthermore, it uses the [Directive Composition API](https://angular.dev/guide/directives/directive-composition-api) instead of inheritance. While this may seem to make some things a little more verbose, it is the better approach to encapsulate the core logic.
### Helper
For an easier setup and maintenance of controls and groups, it is highly recommended to set up the following helpers. Using the helpers provides you with a singular place to edit, in case anything changes. The rest of the guide assumes that you did this. The exact naming of each helper really is up to you.
#### Control Container View Providers
`ControlContainer` is required for all controls and groups that will be used within _ngx-formwork_. To avoid repetition, set up and use this helper. Injection the control container allows the components to use reactive forms stuff, without needing to pass the form group through inputs and wrapping the template into additional tags. See this YouTube Video for more detailed explanation: [How to Make Forms in Angular REUSABLE (Advanced, 2023)](https://www.youtube.com/watch?v=o74WSoJxGPI)
```ts
export const controlContainerViewProviders = [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
];
```#### Control Host Directive
This is a convenience helper to apply the `NgxfwControlDirective`.
```ts
export const ngxfwControlHostDirective = {
directive: NgxfwControlDirective,
inputs: ['content'],
};
```#### Group Host Directive
This is a convenience helper to apply the `NgxfwGroupDirective`.
```ts
export const ngxfwGroupHostDirective = {
directive: NgxfwGroupDirective,
inputs: ['content'],
};
```### Controls
Here is an example of a simple text control.
First create an interface for your control
```ts
export interface TestTextControl extends NgxFwControl {
// Unique Key of your control that is used for differentiating controls
// This can be descriptive like "email-control"
type: 'test-text-control';// Overwrite defaultValue with correct value type
// the default value type is "unknown"
defaultValue?: string;// Additional options only applicable to this control
hint?: string;
}
```Then implement the component. See [Helper](#helper) for how to set up `controlContainerViewProviders` and `ngxfwControlHostDirective`.
> [!IMPORTANT]
> Be sure to bind to `[formControlName]` on the actual input element```ts
@Component({
selector: 'app-test-text-control',
imports: [ReactiveFormsModule],
templateUrl: './test-text-control.component.html',
// Important: You always need view providers
viewProviders: controlContainerViewProviders,
hostDirectives: [
// Convenience declaration
ngxfwControlHostDirective
],
})
export class TestTextControlComponent {
// Inject the Directive to gain access to all public properties
// Make sure to pass the correct type parameter to get proper type information
private readonly control = inject(NgxfwControlDirective);// Extract all things you are interested
// Explicitly setting a type definition is not required, but some IDEs work better if they are present
readonly content: Signal = this.control.content; // The configuration object of the control instance
readonly testId: Signal = this.control.testId;
readonly isHidden: Signal = this.control.isHidden; // Really only should ever be a boolean return value, but an expression could also return a number, string or object
readonly disabled: Signal = this.control.disabled;// We get proper type information when accessing this.content()
readonly hint = computed(() => this.content().hint);
readonly label = computed(() => this.content().label);
readonly id = computed(() => this.content().id);// Getter to easily get access to the underlying form control
// Helpful to check for validation errors
get formControl() {
return this.control.formControl;
}constructor() {
// Optional
// Set visibility handling to manual if you want handle it yourself
// By default it is set to auto and will apply the "hidden" attribute if the control is hidden
this.control.setVisibilityHandling('manual')
}
}
``````html
{{ label() }}
```
Finally, register the control in _app.config.ts_
```ts
componentRegistrations:
{
'test-text-control': TestTextControlComponent
}
```#### Showing Errors
Showing errors works pretty much the same as always. You get access to the form control and then access `hasError`.
In TypeScript set up a getter
```ts
// inject the instance of the directive
private readonly control = inject(NgxfwControlDirective);// Get access to the underlying form control
get formControl() {
return this.control.formControl;
}
```Then, in your template you can do something like this
```html
@if (formControl?.hasError('required')) {
Required
}
```### Groups
Here is an example of a simple group.
First create an interface for your group
```ts
// Unique Key of your group that is used for differentiating groups
// This can be descriptive like "email-control"
export interface TestGroup extends NgxFwFormGroup {
type: 'test-group';// Additional options only applicable to this group if you want
}
```Then implement the component. See [Helper](#helper) for how to set up `controlContainerViewProviders` and `ngxfwGroupHostDirective`.
> [!IMPORTANT]
> Be sure to bind to `[formGroupName]` on an element (e.g. div, ng-container)```ts
@Component({
selector: 'ngxfw-test-group',
imports: [NgxfwAbstractControlDirective, ReactiveFormsModule],
templateUrl: './test-group.component.html',
// Important: You always need view providers
viewProviders: controlContainerViewProviders,
// Convenience declaration
hostDirectives: [ngxfwGroupHostDirective],
})
export class TestGroupComponent {
// Inject the Directive to gain access to all public properties
// Make sure to pass the correct type parameter to get proper type information
private readonly control = inject(NgxfwGroupDirective);// Extract all things you are interested
// Explicitly setting a type definition is not required, but some IDEs work better if they are present
readonly content: Signal = this.control.content; // The configuration object of the group instance
readonly testId: Signal = this.control.testId;
readonly controls: Signal = this.control.controls;
readonly isHidden: Signal = this.control.isHidden; // Really only should ever be a boolean return value, but an expression could also return a number, string or object// Getter to easily get access to the underlying form control
// Helpful to check for validation errors
get formGroup() {
return this.control.formGroup;
}constructor() {
// Optional
// Set visibility handling to manual if you want handle it yourself
// By default it is set to auto and will apply the "hidden" attribute if the control is hidden
this.control.setVisibilityHandling('manual');
}
}
``````html
@for (control of controls(); track control.id) {
}
```Finally, register the group in _app.config.ts_
```ts
componentRegistrations: {
'test-group': TestGroupComponent
}
```#### Showing Errors
Showing errors works pretty much the same as always. You get access to the form control and then access `hasError`.
In TypeScript set up a getter
```ts
// inject the instance of the directive
private readonly control = inject(NgxfwGroupDirective);// Get access to the underlying form group
get formGroup() {
return this.control.formGroup;
}
```Then, in your template you can do something like this
```html
@if (formGroup?.hasError('duplicates')) {
No duplicate values
}
```### Validation
It is possible to create custom validators and combine them.
> [!IMPORTANT]
> You can not combine synchronous validators with async ones!
> Angular itself differentiates between those, so we aim to be consistent with it#### Default Validators
For convenience some static validators, that are built into Angular, are registered by default. Built-in functions that return a validator, like `Validators.minLength` cannot be provided by default, as they require an argument.
The following validators are registered by default:
- Validators.required
- Validators.requiredTrue
- Validators.email
- Validators.nullValidator#### Adding custom validator
_ngx-formwork_ uses the standard Angular validator functions. That means that writing your own is exactly the same as in Angular itself. Checkout ["Defining a custom validator" on the official docs](https://angular.dev/guide/forms/form-validation#defining-custom-validators).
This example uses a forbidden letter validator, that ensure the letter "A" is not used.
```ts
export function forbiddenLetterAValidator(
control: AbstractControl,
): ValidationErrors | null {
const value = control.value;
if (!value) {
return null;
}
if (typeof value !== 'object') {
const includesA = containsText(value, 'a');
return includesA ? { forbiddenLetterA: { value } } : null;
}const values = Object.values(value);
const someValueIncludesA = values.some((v) => containsText(v, 'a'));
return someValueIncludesA ? { forbiddenLetterA: { value } } : null;
}
```You can then register this validator under any name you want.
```ts
validatorRegistrations: {
'forbidden-letter-a': [forbiddenLetterAValidator]
}
```#### Adding custom async validator
Async validators work pretty much the same as synchronous ones.
This example uses a validator, that ensure the text contains the word "async".
> [!NOTE]
> Async validators usually do some checks on the server.
> For this example we only pretend to do so by creating a dummy observable```ts
export function asyncValidator(
control: AbstractControl,
): Observable {
const value = control.value;
if (containsText(value, 'async')) {
return of(null);
}
return of({ async: { value } });
}```
You can then register this validator under any name you want
```ts
asyncValidatorRegistrations: {
async: [asyncValidator]
}
```#### Validators with arguments
Functions that return validators based on arguments can be registered as well.
```ts
validatorRegistrations: {
'min-chars': [Validators.minLength(3)]
}
```#### Combining Validators
Combining validators is as easy as adding them in the same array.
```ts
validatorRegistrations: {
'min-chars': [Validators.minLength(3)],
letter: [letterValidator],
combined: ['min-chars', Validators.required, 'letter'],
}
```## Configuring a form
Once you've registered controls and optionally validators, you write a configuration for a form. You can either do this directly in JSON or in TypeScript, for better typing information.
This example is written in TypeScript
```ts
import { NgxFwContent } from '../../lib';export const exampleForm: NgxFwContent[] = [
// Simple fields with no additional configuration
{
type: 'text',
id: 'name',
label: 'First and Lastname',
},
{
type: 'text',
id: 'company',
label: 'Name of Company',
hint: 'If applicable',
},
{
type: 'numeric',
id: 'licenses',
label: 'Amount of Licenses',
max: 3,
},
// Example how a configuration for a radio button group could look like
{
type: 'radio',
id: 'plan',
label: 'Price Plan',
options: [
{
id: 'plan-123',
label: 'Free',
value: 'p123',
},
{
id: 'plan-456',
label: 'Medium',
value: 'p456',
},
{
id: 'plan-789',
label: 'Large',
value: 'p789',
},
],
},
{
type: 'checkbox',
id: 'termsAccepted',
label: 'I Accept Terms',
},
{
type: 'group',
id: 'repo',
controls: [
{
type: 'text',
id: 'username',
label: 'Username',
default: 'UsernameSuggestion123',
validators: ['min5Characters'],
asyncValidators: ['usernameIsFreeValidator'],
},
{
type: 'numeric',
id: 'repositories',
label: 'Repositories to create',
default: 1,
},
// Example how a configuration for a dropdown could look like
{
type: 'dropdown',
id: 'repoTemplate',
label: 'Template',
default: 'none',
options: [
{
id: 'template-1',
label: 'None',
value: 'none',
},
{
id: 'template-2',
label: 'Monorepo',
value: 'mono',
},
{
id: 'template-3',
label: 'Documentation',
value: 'doc',
},
{
id: 'template-3',
label: 'Note Management',
value: 'note',
},
],
},
{
type: 'checkbox',
id: 'sendConfirmation',
label: 'Send confirmation mail',
default: true,
},
{
type: 'email',
id: 'confirmationMailTarget',
label: 'E-Mail',
hidden: '!repo.sendConfirmation',
hideStrategy: 'remove',
valueStrategy: 'reset',
},
{
type: 'checkbox',
id: 'editProjectId',
label: 'Edit Project ID',
default: false,
},
{
type: 'text',
id: 'projectId',
label: 'Project ID',
default: '123456789',
hidden: '!repo.editProjectId',
hideStrategy: 'keep',
valueStrategy: 'reset',
},
],
},
{
type: 'group',
id: 'docs',
controls: [
{
type: 'numeric',
id: 'docAmount',
label: 'Documents to store',
},
{
type: 'checkbox',
id: 'acceptedLimits',
label: 'I accept the limits for large volumes of documents',
hidden: 'docs.docAmount > 1000',
},
{
type: 'dropdown',
id: 'updateFrequency',
label: 'Documentation Update Frequency',
options: [
{
id: 'on-the-fly',
label: 'On the fly',
value: 'otf',
},
{
id: 'sprint',
label: 'Sprint',
value: 'spr',
},
{
id: 'cycle',
label: 'Cycle',
value: 'cyc',
},
{
id: 'planned',
label: 'Planned',
value: 'pla',
},
],
},
{
type: 'numeric',
id: 'frequency',
label: 'Frequency (Sprint / Cycle Duration)',
hidden: 'docs.docAmount > 2000 && (docs.updateFrequency === "spr" || docs.updateFrequency === "cyc")',
hideStrategy: 'remove',
valueStrategy: 'last',
},
],
},
];```
As you can see the configuration is just an array of controls and/or groups. Every item in that array will be registered on the top-level of the form.
### Create helper type
For better type safety, when writing a form configuration in TypeScript, _ngx-formwork_ provides a helper type *OneOf*.
With this you can construct a union type like this.```ts
export type MyAppControls = OneOf<[TestTextControl, TestGroup]>;
```
and use it like this
```ts
export const exampleForm: MyAppControls[] = [ ... ]
```### Basic Options
The `NgxFwBaseContent` interface is the foundation for all form controls and groups. It defines a common set of options that control registration, validation, visibility, and behavior of the form elements.
- **type** (`string`)
- Specifies the kind of the form control. It determines what control is used and what additional properties may be available.- **id** (`string`)
- Unique identifier for the control. This is used to link configuration with runtime behavior and maintain state consistency.- **validators** (`string[]` - optional)
- An array of strings representing the names of synchronous validators that apply to the control.
- Validators can be registered globally with a validator registration object. (see [Adding Custom Validators](#adding-custom-validator))- **asyncValidators** (`string[]` - optional)
- Similar to `validators`, but for asynchronous validation logic. (see [Adding Custom Async Validators](#adding-custom-async-validator))- **hidden** (`string` - optional)
- A string expression that determines when the control should be hidden.
- This condition is evaluated at runtime to control the visibility of the control.
- The expression is evaluated against the whole form object.- **hideStrategy** (`'keep'` \| `'remove'` - optional)
- Specifies the strategy for handling the control when the `hidden` expression evaluates to true.
- `keep`: The control remains part of the form model (its value and state are retained), despite being hidden.
- `remove`: The control is removed from the form model. This is useful when a hidden value should not be submitted or processed further.- **valueStrategy** (`'last'` \| `'default'` \| `'reset'` - optional)
- Determines how the control’s value is handled when its visibility changes.
- `last`: Preserves the last entered value when the control is hidden and shown again.
- `default`: Reverts the control to its default value upon re-display.
- `reset`: Clears the control's value when it becomes visible.- **disabled** (`string` \| `boolean` - optional)
- Defines whether the control should be disabled.
- Can be a boolean value or a string expression that resolves to a boolean at runtime.
- The expression is evaluated against the whole form object.- **readonly** (`string` \| `boolean` - optional)
- Indicates if the control is read-only, meaning the value is displayed but cannot be modified.
- Accepts either a boolean value or a string expression for dynamic evaluation.
- The expression is evaluated against the whole form object.- **updateOn** (`'change'` \| `'blur'` \| `'submit'` \| `undefined` - optional)
- Specifies the event that triggers an update to the control’s value.
- `change`: Updates the value as the user types (default behavior).
- `blur`: Updates the value when the control loses focus.
- `submit`: Defers the update until the form is submitted.
- If not specified, the default is typically `change`.## Expressions
This documentation explains how expressions are parsed, evaluated, and what features are supported.
### Overview
Expressions are JavaScript snippets provided as strings. They are parsed into an Abstract Syntax Tree (AST) using the Acorn parser. The AST is then evaluated in a controlled environment against the forms value (onValueChange).
### Parsing and Caching
- **Parsing:**
An expression string is parsed to generate an AST. The parser uses a modern ECMAScript version (2022) to support recent JavaScript syntax.- **Caching:**
Parsed ASTs are cached to avoid re-parsing the same expression multiple times. This improves performance when evaluating expressions repeatedly.### Evaluation Process
1. **Supported Node Types:**
- The evaluator supports a range of node types including:
- `Identifier`
- `Literal`
- `ArrayExpression`
- `UnaryExpression`
- `BinaryExpression`
- `LogicalExpression` (supports `&&`, `||`, and nullish coalescing `??`)
- `MemberExpression` (property access with safety checks for null or undefined objects)
- `ConditionalExpression` (ternary operator)
- `ObjectExpression`
- `SequenceExpression`
- `TemplateLiteral`
- `CallExpression` for invoking safe methods
- `ArrowFunctionExpression` for simple arrow functions with expression bodies2. **Evaluation of Operators:**
- **Arithmetic Operators:** `+`, `-`, `*`, `/`, `%`, `**`
- **Comparison Operators:** `<`, `>`, `<=`, `>=`
- **Equality Operators:** `==`, `!=`, `===`, `!==` (follow JavaScript behavior)
- **Bitwise Operators:** `|`, `&`, `^`, `<<`, `>>`, `>>>`
- **Logical Operations:** `&&`, `||`, `??`
- **Safe Method Calls:** When calling methods on objects, the evaluator checks against a whitelist of safe methods (provided for strings, numbers, booleans, and arrays). This ensures that only approved operations are executed.### Limitations
- **Restricted Syntax:**
Complex function bodies or block statements in arrow functions are not supported. Only simple expression-based arrow functions are allowed.- **Restricted Node Types:**
Some JavaScript features are not supported to maintain security and simplicity, such as update expressions, assignments, and using `this` or `super`.- **Controlled Context:**
The evaluation runs only in the context of the form object, avoiding global access and potential security vulnerabilities.## Rendering a form
Now that everything is set up, you can now render the form.
You build the form as usual. This gives you full access to the underlying form, and you can do everything you normally can too.
```ts
@Component({
selector: 'app-some-form',
imports: [ReactiveFormsModule, NgxFwFormComponent],
templateUrl: './form-integration-host.component.html'
})
export class FormIntegrationHostComponent {
// Construct the reactive form as usual
private readonly formBuilder = inject(FormBuilder);
// This is our form configuration. It doesn't have to be passed as an input. You could also have a service that gets this or just import it from a file.
readonly formContent = input.required();
// Building a form with an empty group. All controls and groups are self-registering
// You can also add additional hardcoded control if you want
form = this.formBuilder.group({});// We have normal access to all properties of the form
reset() {
this.form.reset();
}patchValue() {
// Setting the value of the form is done the same way as you normally would
this.form.patchValue({
// Whatever value we want to patch
});
}
}
``````html
SubmitReset
```