Skip to content

Commit

Permalink
Angular 9 compatibility
Browse files Browse the repository at this point in the history
- Use TestBed.inject instead of TestBed.get
- Remove angular-validation-support dependency
- executeAfterContentInit no longer works properly in Angular 9.
Overriding ngAfterContentInit doesn't work properly anymore.
On second thought executeAfterContentInit and ObservableContainer
seem overly complex for what we want to achieve, thus I'm
removing those usages.
- ControlContainer used to be initialised and usable before ngOnInit
was called. This is no longer the case, thus added code to handle this.
  • Loading branch information
davidwalschots committed Jun 6, 2020
1 parent cd6ea1f commit 0d9dcaf
Show file tree
Hide file tree
Showing 15 changed files with 12,913 additions and 7,399 deletions.
21 changes: 0 additions & 21 deletions angular-reactive-validation/package-lock.json

This file was deleted.

20 changes: 7 additions & 13 deletions angular-reactive-validation/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "angular-reactive-validation",
"description": "Reactive Forms validation shouldn't require the developer to write lots of HTML to show validation messages. This library makes it easy.",
"version": "3.0.0",
"version": "4.0.0",
"repository": {
"type": "git",
"url": "https://github.com/davidwalschots/angular-reactive-validation.git"
Expand All @@ -18,24 +18,18 @@
],
"license": "MIT",
"private": false,
"dependencies": {
"angular-validation-support": "2.0.0"
},
"dependencies": {},
"peerDependencies": {
"@angular/core": "^8.0.0",
"@angular/common": "^8.0.0",
"@angular/forms": "^8.0.0",
"rxjs": "^6.5.2"
"@angular/core": "^9.0.0",
"@angular/common": "^9.0.0",
"@angular/forms": "^9.0.0",
"rxjs": "^6.5.4"
},
"ngPackage": {
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "../dist/angular-reactive-validation",
"workingDirectory": "../.ng_build",
"lib": {
"entryFile": "src/public_api.ts",
"umdModuleIds": {
"angular-validation-support": "angular-validation-support"
}
"entryFile": "src/public_api.ts"
},
"whitelistedNonPeerDependencies": [
"."
Expand Down
68 changes: 68 additions & 0 deletions angular-reactive-validation/src/get-control-path.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { TestBed, inject } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';

import { getControlPath } from './get-control-path';

describe('getControlPath', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
FormBuilder
]
});
});

it(`emits paths for form groups`, inject([FormBuilder], (fb: FormBuilder) => {
const firstName = fb.control('');
fb.group({
name: fb.group({
firstName: firstName
})
});

expect(getControlPath(firstName)).toEqual('name.firstName');
}));

it(`emits numeric paths for form arrays`, inject([FormBuilder], (fb: FormBuilder) => {
const firstName = fb.control('');
const firstName2 = fb.control('');

fb.group({
persons: fb.array([
fb.group({
firstName: firstName
}),
fb.group({
firstName: firstName2
})
])
});

expect(getControlPath(firstName)).toEqual('persons.0.firstName');
expect(getControlPath(firstName2)).toEqual('persons.1.firstName');
}));

it(`emits an empty string for a control without parents`, inject([FormBuilder], (fb: FormBuilder) => {
const control = fb.control('');

expect(getControlPath(control)).toEqual('');
}));

it(`emits an index string for a control with only a form array as parent`, inject([FormBuilder], (fb: FormBuilder) => {
const control = fb.control('');

fb.array([control]);

expect(getControlPath(control)).toEqual('0');
}));

it(`emits a single identifier for a control with only a single form group as parent`, inject([FormBuilder], (fb: FormBuilder) => {
const control = fb.control('');

fb.group({
control: control
});

expect(getControlPath(control)).toEqual('control');
}));
});
27 changes: 27 additions & 0 deletions angular-reactive-validation/src/get-control-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AbstractControl } from '@angular/forms';

/**
* Given a control, returns a string representation of the property path to
* this control. Thus, for a FormControl 'firstName', that is part of a
* FormGroup references to as 'name', this function will return: 'name.firstName'.
*
* Note that FormArray indexes are also put in the path, e.g.: 'person.0.name.firstName'.
*/
export function getControlPath(control: AbstractControl): string {
if (control.parent) {
let path = getControlPath(control.parent);
if (path) {
path += '.';
}
return path + Object.keys(control.parent.controls).find(key => {
const controls = control.parent.controls;
if (Array.isArray(controls)) {
return controls[Number(key)] === control;
} else {
return controls[key] === control;
}
});
}

return '';
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormGroup, FormControl, ControlContainer } from '@angular/forms';
import { FormGroup, FormControl, ControlContainer, FormGroupDirective } from '@angular/forms';

export function getFormControlFromContainer(name: string, controlContainer: ControlContainer | undefined): FormControl {
if (controlContainer) {
Expand All @@ -20,6 +20,11 @@ export function getFormControlFromContainer(name: string, controlContainer: Cont
}
}

export function isControlContainerVoidOrInitialized(controlContainer: ControlContainer | undefined) {
return !!(!controlContainer || (<FormGroupDirective>controlContainer).form ||
(controlContainer.formDirective && (<FormGroupDirective>controlContainer.formDirective).form));
}

function getPath(controlContainer: ControlContainer): string[] {
return controlContainer.path || [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe(`ReactiveValidationModule`, () => {
});

it(`should not provide configuration`, () => {
expect(() => TestBed.get(ReactiveValidationModuleConfigurationToken)).toThrowError(/No provider for/);
expect(() => TestBed.inject(ReactiveValidationModuleConfigurationToken)).toThrowError(/No provider for/);
});
});

Expand All @@ -29,7 +29,7 @@ describe(`ReactiveValidationModule`, () => {
});

it(`should provide configuration`, () => {
expect(TestBed.get(ReactiveValidationModuleConfigurationToken)).toEqual(configuration);
expect(TestBed.inject(ReactiveValidationModuleConfigurationToken)).toEqual(configuration);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ReactiveValidationModuleConfigurationToken } from './reactive-validatio
]
})
export class ReactiveValidationModule {
static forRoot(configuration?: ReactiveValidationModuleConfiguration): ModuleWithProviders {
static forRoot(configuration?: ReactiveValidationModuleConfiguration): ModuleWithProviders<ReactiveValidationModule> {
return {
ngModule: ReactiveValidationModule,
providers: [{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { TestBed, ComponentFixture } from '@angular/core/testing';

import { ValidationMessageComponent } from './validation-message.component';
import { ValidationError } from '../validation-error';
import { Validators } from '../validators';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';

describe('ValidationMessageComponent', () => {
describe('canHandle', () => {
Expand Down Expand Up @@ -115,4 +117,36 @@ describe('ValidationMessageComponent', () => {
}
}
});

it('can set control by name without exception being thrown due to ControlContainer not yet being initialized', () => {
@Component({
template: `
<form [formGroup]="form">
<arv-validation-message for="age" key="min">
</arv-validation-message>
</form>
`
})
class TestHostComponent {
age = new FormControl(0, [
Validators.min(10, 'invalid age')
]);
form = new FormGroup({
age: this.age
});

@ViewChild(ValidationMessageComponent, { static: true }) validationMessageComponent: ValidationMessageComponent;
}

TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [ValidationMessageComponent, TestHostComponent]
});

expect(() => {
const fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
expect(fixture.componentInstance.validationMessageComponent.for).toBe(fixture.componentInstance.age);
}).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Component, Input, ViewEncapsulation, Optional } from '@angular/core';
import { Component, Input, ViewEncapsulation, Optional, OnInit } from '@angular/core';
import { FormControl, ValidationErrors, ControlContainer } from '@angular/forms';

import { ValidationError } from '../validation-error';
import { getFormControlFromContainer } from '../get-form-control-from-container';
import { getFormControlFromContainer, isControlContainerVoidOrInitialized } from '../get-form-control-from-container';

@Component({
selector: 'arv-validation-message',
Expand All @@ -15,7 +15,7 @@ import { getFormControlFromContainer } from '../get-form-control-from-container'
*
* TODO: Trigger revalidation by parent whenever [for] changes.
*/
export class ValidationMessageComponent {
export class ValidationMessageComponent implements OnInit {
private _context: ValidationErrors | undefined;
private _for: FormControl | undefined;

Expand All @@ -27,6 +27,10 @@ export class ValidationMessageComponent {
* ValidationMessagesComponent has multiple FormControls specified.
*/
set for(control: FormControl | string | undefined) {
if (!isControlContainerVoidOrInitialized(this.controlContainer)) {
this.initializeForOnInit = () => this.for = control;
return;
}
this._for = typeof control === 'string' ? getFormControlFromContainer(control, this.controlContainer) : control;
}
get for(): FormControl | string | undefined {
Expand All @@ -39,6 +43,8 @@ export class ValidationMessageComponent {
*/
key: string | undefined;

private initializeForOnInit = () => {};

/**
* The ValidationErrors object that contains contextual information about the error, which can be used for
* displaying, e.g. the minimum length within the error message.
Expand All @@ -47,6 +53,10 @@ export class ValidationMessageComponent {
return this._context;
}

ngOnInit() {
this.initializeForOnInit();
}

canHandle(error: ValidationError) {
return (!this.for || error.control === this.for) && error.key === this.key;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ControlContainer, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
import { ControlContainer, FormGroup, FormControl, ReactiveFormsModule, FormGroupDirective } from '@angular/forms';
import { Subject } from 'rxjs';

import { ValidationMessagesComponent } from './validation-messages.component';
Expand Down Expand Up @@ -33,14 +33,13 @@ describe('ValidationMessagesComponent', () => {
lastName: lastNameControl
});

const controlContainer: any = {
control: formGroup
};
const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []);
formGroupDirective.form = formGroup;

TestBed.configureTestingModule({
declarations: [ValidationMessagesComponent],
providers: [
{ provide: ControlContainer, useValue: controlContainer }
{ provide: ControlContainer, useValue: formGroupDirective }
]
});

Expand Down Expand Up @@ -241,6 +240,39 @@ describe('ValidationMessagesComponent', () => {
.toThrowError(`There is no suitable arv-validation-message element to show the 'required' error of ''`);
});

it('can set control by name without exception being thrown due to ControlContainer not yet being initialized', () => {
@Component({
template: `
<form [formGroup]="form">
<input type="number" formControlName="age">
<arv-validation-messages for="age"></arv-validation-messages>
</form>
`
})
class TestHostComponent {
age = new FormControl(0, [
Validators.min(10, 'invalid age')
]);
form = new FormGroup({
age: this.age
});

@ViewChild(ValidationMessagesComponent, { static: true }) validationMessagesComponent: ValidationMessagesComponent;
}

TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent]
});

expect(() => {
const fixture = TestBed.createComponent(TestHostComponent);
fixture.componentInstance.age.markAsTouched();
fixture.detectChanges();
expect(fixture.componentInstance.validationMessagesComponent.getErrorMessages()).toEqual(['invalid age']);
}).not.toThrow();
});

xdescribe('', () => {
let onerrorBeforeTest: OnErrorEventHandler;
beforeEach(() => {
Expand Down
Loading

0 comments on commit 0d9dcaf

Please sign in to comment.