Reusable Angular forms in no time
Tags: angular, typescript, form, tutorial
Published: 2020-06-07 12:30:55

Imagine this situation:
>Your team lead assigns you the task to create a list view (e.g. 
 a list of orders) and a form view to edit the list entries. You find out, that the product area which is also part of the software provides the same logic. You start with copying the code of the list view (which is fortunately just an separated component which only needs a data source passed in). Then you start copying step by step the form template. A textfield here, a checkbox there. Oh, I need a select field and the other template doesn't contain it - no prob, HTML is nice AF and I can write the markup on my own. After a while you're done and the form works as expected.

Recognize yourself in that workflow? Me too.

## What's wrong with it?
Let's check what are the negative aspects of this:
1. If you copy code you're violating the [D.R.Y. principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
2. The style of fields may vary since you need to write some markup on your own.
3. If you want to add a tooltip to all text fields you've to edit all files containing a text field. (You will forget one, believe me 😅)
4. Your edit form is violating the [Single-responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) since it must validate all fields against general business logic (Not empty, min, max etc), contains listeners of field selection etc.

## How to make it better?
First of all you can create simple components for each field. For example a `TextFieldComponent` or a `CheckboxComponent`. This would solve problem 1, 3 and partially 2. Why partially 2? Because it's still possible to modify the outer markup in the html where you use the component.

## What else? Forms without writing HTML? No way!
You're right. Without writing any HTML it's not possible but you can reduce the HTML to a minimum.
I created a Angular library which is called [`@mintware-de/form-builder`](https://github.com/mintware-de/mw-form-builder).
It provides all functionality you need to create reusable, maintainable and extensible forms in Angular.

## Real world example
Create a new minimal Angular project using 
```bash
ng new form-example --minimal --skip-tests --inline-style --inline-template  
```

Install the package
```bash
npm i -d @mintware-de/form-builder@^2.0.0
```

### Create a form fields module
In a previous section I mentioned that's an good idea to separate fields into it's own components. First, create a new module called `form-fields` inside your `src/app` directory and `cd` into the module directory. 

```bash
ng g m form-fields
cd src/app/form-fields
```

As described in the [getting started](https://github.com/mintware-de/mw-form-builder/blob/master/docs/getting-started.md) import and export the `FormBuilderModule` and the `ReactiveFormsModule` in your `FormFieldsModule`.

```typescript
// ...
  imports: [
    CommonModule,
    FormBuilderModule,   // New
    ReactiveFormsModule  // New
  ],
  exports: [
    FormBuilderModule,   // New
    ReactiveFormsModule  // New
  ]
// ...
```

Import the `FormFieldsModule` in your `AppModule`.

### Creating form field components
Let's start with creating a text field component which has a inline template, inline styles and no tests. 

```bash
ng g c text-field --inline-template --inline-style --skip-tests
```

_Why using inline template?
The template of the form components are really small in the most cases.
Additionally to this, mostly you don't need to write TypeScript code in the component itself. 
That's the reason why I prefer inline templates._

#### Create the options interface and the form type
A form component for the form builder consists of 3 parts:
1. The Angular component
2. An options interface which is used for configuring the component
3. The form type which connects the component and the options. The form type also defines the validation rules.

Now create the options and the type next to the text-field.component.

```
|- src/app/form-fields
|  |- text-field
|  |  |- text-field.component.ts
|  |  |- text-field.options.ts    <-- New
|  |  |- text-field.type.ts       <-- New
```

Create an empty interface for the text field options. We will add needed properties later.
```typescript
// text-field.options.ts
export interface TextFieldOptions {

}
```

The form type must extend the `AbstractType<TOptions>` class. The naming convention for the class name is the PascalCased file name without the suffix. In this case simply `TextField`.

As `TOptions` you need to pass the created `TextFieldOptions` type and implement the abstract member `component`. Set the value to `TextFieldComponent`.
The referenced component will be used to render the form field.

The complete file should look like this:
```typescript
// text-field.type.ts
import {AbstractType, Constructor} from '@mintware-de/form-builder';
import {TextFieldOptions} from './text-field.options';
import {TextFieldComponent} from './text-field.component';

export class TextField extends AbstractType<TextFieldOptions> {
    public readonly component: Constructor = TextFieldComponent;
}
```

#### Write the component code

First of all, add the `TextFieldComponent` to the `FormFieldsModule` inside the `entryComponents` section.
This is necessary since the form builder render the components
with a `ComponentFactory`.

```typescript
// form-fields.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TextFieldComponent } from './text-field/text-field.component';

@NgModule({
  declarations: [
    TextFieldComponent,
  ],
  entryComponents: [
    TextFieldComponent, // New
  ],
  imports: [
    CommonModule
  ]
})
export class FormFieldsModule {
}
```

Open the `TextFieldComponent` and replace the `implements OnInit` with
`extends AbstractFormFieldComponent<AbstractType<TextFieldOptions>>`
```typescript
// text-field.component.ts
import { Component } from '@angular/core'; // modified
import {
  AbstractFormFieldComponent,
  AbstractType
} from '@mintware-de/form-builder'; // new
import { TextFieldOptions } from './text-field.options'; // new

@Component({
  selector: 'app-text-field',
  template: `
    <p>
      text-field works!
    </p>
  `,
  // modified
})
export class TextFieldComponent
  extends AbstractFormFieldComponent<AbstractType<TextFieldOptions>> { // modified

  // modified

}
```

_Why not using `extends TextField` directly?
Since the `TextField` imports the `TextFieldComponent` a direct use of `TextField` inside the `TextFieldComponent` would cause a circular reference._

#### Add the input field
Now we need to add some HTML code which contains the input element. We use the `[formControl]` attribute on the input to link the input element with the `FormControl` in the Angular `FormGroup`.
The `AbstractFormGroupComponent` a property `mwElement` which contains the form control.

Update the template of the `TextFieldComponent`:
```html
<input type="text" [formControl]="mwElement">
```

Congrats, you just created your first form field 👏. Let's create a form to use **and reuse** the the form field.


#### Create a form and use the form field

Open the `AppComponent` and replace the content with this:
```typescript
import {Component} from '@angular/core';
import {FormModel} from '@mintware-de/form-builder';
import {TextField} from './form-fields/text-field/text-field.type';

@Component({
  selector: 'app-root',
  // Display the form by using the MwFormBuilder Component
  // Pass the formModel and formData and set a submit action
  // The action is only called if the form is valid
  template: `
    <mw-form-builder #myForm
                     [mwFormModel]="formModel"
                     [mwFormData]="formData"
                     (mwFormSubmit)="submit($event)">
    </mw-form-builder>

    <button type="button" (click)="myForm.submit()">Submit</button>
  `
})
export class AppComponent {
  // Create a form model. 
  // The naming and nesting is equal to the formData
  public formModel: FormModel = {
    firstName: new TextField({}),
    lastName: new TextField({})
  };

  // Set the initial form data
  public formData: { firstName: string, lastName: string } = {
    firstName: 'John',
    lastName: 'Doe',
  };

  // Create a submit handler
  public submit(data: { firstName: string, lastName: string }): void {
    console.log("Form was submitted: %o", data);
  }
}
```

Run `ng serve` to start the app. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qwexmiixhn6nok7xiz4g.png)

Press the button and something like
`Form was submitted: {firstName: "John", lastName: "Doe"}` 
is written to the console.

### Adding options to the text field
Cool, text fields without labels. That's what I call usability 😬
Ok, let's add a few options to our text field:
- Label: The string which is used as the label
- Required: A boolean which defaults to true and marks the field as required or not. 

Edit the `TextFieldOptions` interface and add the fields:
```typescript
// text-field.options.ts
export interface TextFieldOptions {
  label: string;      // new
  required?: boolean; // new
}
```

Now update the HTML code of the TextFieldComponent and use the properties. You can access the options object in the `mwFieldType` property, which comes from `AbstractFormFieldComponent`.

```html
<div>
  <label [for]="mwPath">{{ mwFieldType.options.label }}</label>
  <input type="text"
         [formControl]="mwElement"
         [id]="mwPath"
         [required]="mwFieldType.options.required">
   <div *ngIf="mwElement.errors && mwElement.errors.required">
       {{mwFieldType.options.label}} is required.
   </div>
</div>
```

Since the `label` property is not nullable, you've to set it in the form model in the `AppComponent`.
```typescript
public formModel: FormModel = {
  firstName: new TextField({
    label: 'First name', // new
  }),
  lastName: new TextField({
    label: 'Last name',  // new
  })
};
```

Reload the page and voilà, the form fields has labels:
![Form with labels](https://dev-to-uploads.s3.amazonaws.com/i/x28w7a6gjlcw7vu07bv7.png)

Almost done. We just need to add the required validation and set the default state to true.

### Option defaults & validation
To set default values for optional options, you need to add a constructor to the `TextField` type. In the constructor you can use `Object.assign({}, ...);` to set the defaults in the options object. Validators can be added by overriding the `validators` getter.

```typescript
import {AbstractType, Constructor} from '@mintware-de/form-builder';
import {TextFieldOptions} from './text-field.options';
import {TextFieldComponent} from './text-field.component';
import {ValidatorFn, Validators} from '@angular/forms';

export class TextField extends AbstractType<TextFieldOptions> {
  public readonly component: Constructor = TextFieldComponent;

  constructor(opts: TextFieldOptions) {
    // Set the field defaults
    super(Object.assign({
      required: true,
    }, opts));
  }

  public get validators(): ValidatorFn[] {
    const validators: ValidatorFn[] = [];

    // Only add the required validator if the field is required
    if (this.options.required) {
      validators.push(Validators.required);
    }

    return validators;
  }
}
```

Reload the page and clear the inputs. You should see the error message and if you try to submit the form, the `submit` method is not called since both fields are required by default.

## FAQ
### Is the form builder compatible with Angular Material?  
Yep

### Can I create collection fields and nested forms?
Collections or Array fields and nested forms are both supported.
Guides:
- [Collections](https://github.com/mintware-de/mw-form-builder/blob/master/docs/collection-form-fields.md)
- [Form groups](https://github.com/mintware-de/mw-form-builder/blob/master/docs/form-groups.md) for nested forms.

### ...Complex layouts?
Yes, there is a special FormType called `AbstractLayoutType`. Guide: [Layout types](https://github.com/mintware-de/mw-form-builder/blob/master/docs/layout-types.md) 

Example: https://gist.github.com/devtronic/807e8bfc712330ef13a5c9b8bf5a71cf

------

I hope everything was clear and you enjoyed reading my post.

Questions, Suggestions etc. ➡️ Comments