# Angular 15 directive - trim (input) on blur

In this article, I'll present the process of creating and testing Angular 15 directive. The directive **trim on blur** will be responsible for removing white spaces after firing the **blur** event on the current element. The supported elements are **input** and **textarea**. Everything will be based on **Angular Forms** mechanics.

## Requirements

Installed libraries: NodeJs 16, NPM 8, npx

The whole project was set up in **Linux** environment.

## Project initialization

Install Angular CLI version 15

```shell
npm install -g @angular/cli@15
```

Create new project

```shell
ng new app --minimal=true --defaults=true
```

A detailed description of the options: https://angular.io/cli/new

Navigate to the project directory

```shell
cd app
```

Install additional dependencies

```shell
npm i jest@29 @types/jest@29 @angular/material@15 jest-preset-angular@13
```

The how to integrate **Jest** with **Angular** can be found here: https://github.com/thymikee/jest-preset-angular

## Create directive

The directive will be created and exported as a separate module.

Generate module:

```shell
ng generate module directives/trim-on-blur
```

Generate directive:

```shell
ng generate directive --export=true directives/trim-on-blur/trim-on-blur
```

## Implement directive

Open file **app/src/app/directives/trim-on-blur/trim-on-blur.directive.ts**

Start with the **selector's** modification. The directive will only be used with **textarea** and **input** elements.

```typescript
@Directive({
  selector: 'textarea[appTrimOnBlur], input[appTrimOnBlur]',
})
```

Create a constructor and add two dependencies:

```typescript
  constructor(
    @Optional() private formControlDir: FormControlDirective,
    @Optional() private formControlName: FormControlName
  ) {}
```

* **formControlDir: FormControlDirective** - used in conjunction with **formControl**,
    
* **formControlName: FormControlName** - used in conjunction with **formControlName**,
    

Only one of two dependencies will be injected into our directive.

Create the **onBlur** method and decorate it with **@HostListener('blur')** decorator in order to take advantage of the native **blur** event.

```typescript
  @HostListener('blur')
  onBlur(): void {
  }
```

Implement **blur** event handler.

```typescript
  @HostListener('blur')
  onBlur(): void {
    const control = this.formControlDir?.control || this.formControlName?.control;
    if (!control) {
      return;
    }

    const value = control.value;
    if (value == null) {
      return;
    }

    const trimmed = value.trim();
    control.patchValue(trimmed);
  }
```

The method works as follows:

* get the control from the dependencies and if it's not present then return
    

```typescript
    const control = this.formControlDir?.control || this.formControlName?.control;
    if (!control) {
      return;
    }
```

* if the value is **null** or **undefined** then return
    

```typescript
    const value = control.value;
    if (value == null) {
      return;
    }
```

* remove whitespaces from the start and the end of the current value and update the control
    

```typescript
    const trimmed = value.trim();
    control.patchValue(trimmed);
```

## Use directive

Modify the file **app/src/app/app.component.ts** by declaring controls.

```typescript
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styles: []
})
export class AppComponent {
  textAreaControl = new FormControl();
  inputControl = new FormControl();

  form = new FormGroup({
    input: new FormControl(),
    textarea: new FormControl(),
  });
}
```

Create view **app/src/app/app.component.html** and add form fields.

```typescript
<div style="display: flex; flex-direction: column; width: 400px">
<mat-form-field>
  <mat-label>Textarea formControl</mat-label>
  <textarea
    [formControl]="textAreaControl"
    appTrimOnBlur
    matInput
  ></textarea>
</mat-form-field>

<mat-form-field>
  <mat-label>Input formControl</mat-label>
  <input [formControl]="inputControl" appTrimOnBlur matInput/>
</mat-form-field>

  <form [formGroup]="form">
    <mat-form-field>
      <mat-label>Textarea formControlName</mat-label>
      <textarea
        formControlName='textarea'
        appTrimOnBlur
        matInput
      ></textarea>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Input formControlName</mat-label>
      <input formControlName='input' appTrimOnBlur matInput/>
    </mat-form-field>
  </form>
</div>
```

Start the application by running:

```shell
npm start
```

Navigate to **http://localhost:4200/** and verify **manually** if the directive works.

## Testing

As part of the tests, three groups of controls will be tested:

* with **formControl**
    
* with **formControlName**
    
* without any of the above
    

Create file **app/src/app/directives/trim-on-blur/trim-on-blur.directive.spec.ts** and add the following code:

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatInputHarness } from '@angular/material/input/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CommonModule } from '@angular/common';
import { TrimOnBlurModule } from './trim-on-blur.module';

describe('TrimOnBlurModule', () => {
  let fixture: ComponentFixture<TestComponent>;
  let loader: HarnessLoader;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        NoopAnimationsModule,
        MatFormFieldModule,
        ReactiveFormsModule,
        MatInputModule,
        TrimOnBlurModule
      ],
      declarations: [TestComponent],
    });

    fixture = TestBed.createComponent(TestComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  describe('when component is initialized', () => {

    describe('test formControl', () => {
      describe('test textarea', () => {
        describe('and user sets "test me" value', () => {
          beforeEach(async () => {
            const input = await getTextareaHarness(formControlWrapperAncestor);
            await input.focus();
            await input.setValue('test me');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getTextareaHarness(formControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });

        describe('and user sets "  test me  " value', () => {
          beforeEach(async () => {
            const input = await getTextareaHarness(formControlWrapperAncestor);
            await input.focus();
            await input.setValue('  test me  ');
            await input.blur();
            fixture.detectChanges();
          });

          it('should update control value', async () => {
            const input = await getTextareaHarness(formControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });
      });

      describe('test input', () => {
        describe('and user sets "test me" value', () => {
          beforeEach(async () => {
            const input = await getInputHarness(formControlWrapperAncestor);
            await input.focus();
            await input.setValue('test me');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getInputHarness(formControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });

        describe('and user sets "  test me  " value', () => {
          beforeEach(async () => {
            const input = await getInputHarness(formControlWrapperAncestor);
            await input.focus();
            await input.setValue('  test me  ');
            await input.blur();
            fixture.detectChanges();
          });

          it('should update control value', async () => {
            const input = await getInputHarness(formControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });
      });
    })

    describe('test formControlName', () => {
      describe('test textarea', () => {
        describe('and user sets "test me" value', () => {
          beforeEach(async () => {
            const input = await getTextareaHarness(formControlNameWrapperAncestor);
            await input.focus();
            await input.setValue('test me');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getTextareaHarness(formControlNameWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });

        describe('and user sets "  test me  " value', () => {
          beforeEach(async () => {
            const input = await getTextareaHarness(formControlNameWrapperAncestor);
            await input.focus();
            await input.setValue('  test me  ');
            await input.blur();
            fixture.detectChanges();
          });

          it('should update control value', async () => {
            const input = await getTextareaHarness(formControlNameWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });
      });

      describe('test input', () => {
        describe('and user sets "test me" value', () => {
          beforeEach(async () => {
            const input = await getInputHarness(formControlNameWrapperAncestor);
            await input.focus();
            await input.setValue('test me');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getInputHarness(formControlNameWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });

        describe('and user sets "  test me  " value', () => {
          beforeEach(async () => {
            const input = await getInputHarness(formControlNameWrapperAncestor);
            await input.focus();
            await input.setValue('  test me  ');
            await input.blur();
            fixture.detectChanges();
          });

          it('should update control value', async () => {
            const input = await getInputHarness(formControlNameWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });
      });
    });

    describe('test elements without form control', () => {
      describe('test textarea', () => {
        describe('and user sets "test me" value', () => {
          beforeEach(async () => {
            const input = await getTextareaHarness(noControlWrapperAncestor);
            await input.focus();
            await input.setValue('test me');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getTextareaHarness(noControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });

        describe('and user sets "  test me  " value', () => {
          beforeEach(async () => {
            const input = await getTextareaHarness(noControlWrapperAncestor);
            await input.focus();
            await input.setValue('  test me  ');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getTextareaHarness(noControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('  test me  ');
          });
        });
      });

      describe('test input', () => {
        describe('and user sets "test me" value', () => {
          beforeEach(async () => {
            const input = await getInputHarness(noControlWrapperAncestor);
            await input.focus();
            await input.setValue('test me');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getInputHarness(noControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('test me');
          });
        });

        describe('and user sets "  test me  " value', () => {
          beforeEach(async () => {
            const input = await getInputHarness(noControlWrapperAncestor);
            await input.focus();
            await input.setValue('  test me  ');
            await input.blur();
            fixture.detectChanges();
          });

          it('should not change control value', async () => {
            const input = await getInputHarness(noControlWrapperAncestor);
            const value = await input.getValue();
            expect(value).toBe('  test me  ');
          });
        });
      });
    });


  });

  const getTextareaHarness = (ancestor: string): Promise<MatInputHarness> => {
    return loader.getHarness(
      MatInputHarness.with({
        ancestor,
      })
    );
  };

  const getInputHarness = (ancestor: string): Promise<MatInputHarness> => {
    return loader.getHarness(
      MatInputHarness.with({
        ancestor,
      })
    );
  };
});

const formControlWrapperAncestor = '.form-control-wrapper';
const formControlNameWrapperAncestor = '.form-control-name-wrapper';
const noControlWrapperAncestor = '.no-control-wrapper';

@Component({
  selector: 'app-test',
  template: `
    <div class="form-control-wrapper">
      <mat-form-field>
        <mat-label>Textarea formControl</mat-label>
        <textarea
          [formControl]="textAreaControl"
          appTrimOnBlur
          matInput
        ></textarea>
      </mat-form-field>

      <mat-form-field>
        <mat-label>Input formControl</mat-label>
        <input [formControl]="inputControl" appTrimOnBlur matInput/>
      </mat-form-field>
    </div>

    <div class="form-control-name-wrapper">
      <form [formGroup]="form">
        <mat-form-field>
          <mat-label>Textarea formControlName</mat-label>
          <textarea
            formControlName='textarea'
            appTrimOnBlur
            matInput
          ></textarea>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Input formControlName</mat-label>
          <input formControlName='input' appTrimOnBlur matInput/>
        </mat-form-field>
      </form>
    </div>

    <div class="no-control-wrapper">
      <textarea appTrimOnBlur matInput></textarea>
      <input appTrimOnBlur matInput/>
    </div>
  `,
})
export class TestComponent {
  textAreaControl = new FormControl();
  inputControl = new FormControl();

  form = new FormGroup({
    input: new FormControl(),
    textarea: new FormControl(),
  });
}
```

The initial **beforeEach** is responsible for initialization. Remember to import all required modules.

```typescript
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        NoopAnimationsModule,
        MatFormFieldModule,
        ReactiveFormsModule,
        MatInputModule,
        TrimOnBlurModule
      ],
      declarations: [TestComponent],
    });

    fixture = TestBed.createComponent(TestComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
  });
```

The definition of the test component does not require any extra explanation. It's just a simple component with controls that are being tested.

```typescript
@Component({
  selector: 'app-test',
  template: `
    <div class="form-control-wrapper">
      <mat-form-field>
        <mat-label>Textarea formControl</mat-label>
        <textarea
          [formControl]="textAreaControl"
          appTrimOnBlur
          matInput
        ></textarea>
      </mat-form-field>

      <mat-form-field>
        <mat-label>Input formControl</mat-label>
        <input [formControl]="inputControl" appTrimOnBlur matInput/>
      </mat-form-field>
    </div>

    <div class="form-control-name-wrapper">
      <form [formGroup]="form">
        <mat-form-field>
          <mat-label>Textarea formControlName</mat-label>
          <textarea
            formControlName='textarea'
            appTrimOnBlur
            matInput
          ></textarea>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Input formControlName</mat-label>
          <input formControlName='input' appTrimOnBlur matInput/>
        </mat-form-field>
      </form>
    </div>

    <div class="no-control-wrapper">
      <textarea appTrimOnBlur matInput></textarea>
      <input appTrimOnBlur matInput/>
    </div>
  `,
})
export class TestComponent {
  textAreaControl = new FormControl();
  inputControl = new FormControl();

  form = new FormGroup({
    input: new FormControl(),
    textarea: new FormControl(),
  });
}
```

To test **angular material** elements use **MatHarness**.

```typescript
  const getTextareaHarness = (ancestor: string): Promise<MatInputHarness> => {
    return loader.getHarness(
      MatInputHarness.with({
        ancestor,
      })
    );
  };

  const getInputHarness = (ancestor: string): Promise<MatInputHarness> => {
    return loader.getHarness(
      MatInputHarness.with({
        ancestor,
      })
    );
  };
```

Run the tests:

```shell
npx jest
```

# Source code

https://gitlab.com/barcioch-blog-examples/20230314-angular-directive-trim-on-blur-example
