Skip to content

Commit 0d9dcaf

Browse files
Angular 9 compatibility
- 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.
1 parent cd6ea1f commit 0d9dcaf

15 files changed

+12913
-7399
lines changed

angular-reactive-validation/package-lock.json

Lines changed: 0 additions & 21 deletions
This file was deleted.

angular-reactive-validation/package.json

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "angular-reactive-validation",
33
"description": "Reactive Forms validation shouldn't require the developer to write lots of HTML to show validation messages. This library makes it easy.",
4-
"version": "3.0.0",
4+
"version": "4.0.0",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/davidwalschots/angular-reactive-validation.git"
@@ -18,24 +18,18 @@
1818
],
1919
"license": "MIT",
2020
"private": false,
21-
"dependencies": {
22-
"angular-validation-support": "2.0.0"
23-
},
21+
"dependencies": {},
2422
"peerDependencies": {
25-
"@angular/core": "^8.0.0",
26-
"@angular/common": "^8.0.0",
27-
"@angular/forms": "^8.0.0",
28-
"rxjs": "^6.5.2"
23+
"@angular/core": "^9.0.0",
24+
"@angular/common": "^9.0.0",
25+
"@angular/forms": "^9.0.0",
26+
"rxjs": "^6.5.4"
2927
},
3028
"ngPackage": {
3129
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3230
"dest": "../dist/angular-reactive-validation",
33-
"workingDirectory": "../.ng_build",
3431
"lib": {
35-
"entryFile": "src/public_api.ts",
36-
"umdModuleIds": {
37-
"angular-validation-support": "angular-validation-support"
38-
}
32+
"entryFile": "src/public_api.ts"
3933
},
4034
"whitelistedNonPeerDependencies": [
4135
"."
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { TestBed, inject } from '@angular/core/testing';
2+
import { FormBuilder } from '@angular/forms';
3+
4+
import { getControlPath } from './get-control-path';
5+
6+
describe('getControlPath', () => {
7+
beforeEach(() => {
8+
TestBed.configureTestingModule({
9+
providers: [
10+
FormBuilder
11+
]
12+
});
13+
});
14+
15+
it(`emits paths for form groups`, inject([FormBuilder], (fb: FormBuilder) => {
16+
const firstName = fb.control('');
17+
fb.group({
18+
name: fb.group({
19+
firstName: firstName
20+
})
21+
});
22+
23+
expect(getControlPath(firstName)).toEqual('name.firstName');
24+
}));
25+
26+
it(`emits numeric paths for form arrays`, inject([FormBuilder], (fb: FormBuilder) => {
27+
const firstName = fb.control('');
28+
const firstName2 = fb.control('');
29+
30+
fb.group({
31+
persons: fb.array([
32+
fb.group({
33+
firstName: firstName
34+
}),
35+
fb.group({
36+
firstName: firstName2
37+
})
38+
])
39+
});
40+
41+
expect(getControlPath(firstName)).toEqual('persons.0.firstName');
42+
expect(getControlPath(firstName2)).toEqual('persons.1.firstName');
43+
}));
44+
45+
it(`emits an empty string for a control without parents`, inject([FormBuilder], (fb: FormBuilder) => {
46+
const control = fb.control('');
47+
48+
expect(getControlPath(control)).toEqual('');
49+
}));
50+
51+
it(`emits an index string for a control with only a form array as parent`, inject([FormBuilder], (fb: FormBuilder) => {
52+
const control = fb.control('');
53+
54+
fb.array([control]);
55+
56+
expect(getControlPath(control)).toEqual('0');
57+
}));
58+
59+
it(`emits a single identifier for a control with only a single form group as parent`, inject([FormBuilder], (fb: FormBuilder) => {
60+
const control = fb.control('');
61+
62+
fb.group({
63+
control: control
64+
});
65+
66+
expect(getControlPath(control)).toEqual('control');
67+
}));
68+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { AbstractControl } from '@angular/forms';
2+
3+
/**
4+
* Given a control, returns a string representation of the property path to
5+
* this control. Thus, for a FormControl 'firstName', that is part of a
6+
* FormGroup references to as 'name', this function will return: 'name.firstName'.
7+
*
8+
* Note that FormArray indexes are also put in the path, e.g.: 'person.0.name.firstName'.
9+
*/
10+
export function getControlPath(control: AbstractControl): string {
11+
if (control.parent) {
12+
let path = getControlPath(control.parent);
13+
if (path) {
14+
path += '.';
15+
}
16+
return path + Object.keys(control.parent.controls).find(key => {
17+
const controls = control.parent.controls;
18+
if (Array.isArray(controls)) {
19+
return controls[Number(key)] === control;
20+
} else {
21+
return controls[key] === control;
22+
}
23+
});
24+
}
25+
26+
return '';
27+
}

angular-reactive-validation/src/get-form-control-from-container.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FormGroup, FormControl, ControlContainer } from '@angular/forms';
1+
import { FormGroup, FormControl, ControlContainer, FormGroupDirective } from '@angular/forms';
22

33
export function getFormControlFromContainer(name: string, controlContainer: ControlContainer | undefined): FormControl {
44
if (controlContainer) {
@@ -20,6 +20,11 @@ export function getFormControlFromContainer(name: string, controlContainer: Cont
2020
}
2121
}
2222

23+
export function isControlContainerVoidOrInitialized(controlContainer: ControlContainer | undefined) {
24+
return !!(!controlContainer || (<FormGroupDirective>controlContainer).form ||
25+
(controlContainer.formDirective && (<FormGroupDirective>controlContainer.formDirective).form));
26+
}
27+
2328
function getPath(controlContainer: ControlContainer): string[] {
2429
return controlContainer.path || [];
2530
}

angular-reactive-validation/src/reactive-validation.module.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe(`ReactiveValidationModule`, () => {
1212
});
1313

1414
it(`should not provide configuration`, () => {
15-
expect(() => TestBed.get(ReactiveValidationModuleConfigurationToken)).toThrowError(/No provider for/);
15+
expect(() => TestBed.inject(ReactiveValidationModuleConfigurationToken)).toThrowError(/No provider for/);
1616
});
1717
});
1818

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

3131
it(`should provide configuration`, () => {
32-
expect(TestBed.get(ReactiveValidationModuleConfigurationToken)).toEqual(configuration);
32+
expect(TestBed.inject(ReactiveValidationModuleConfigurationToken)).toEqual(configuration);
3333
});
3434
});
3535
});

angular-reactive-validation/src/reactive-validation.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { ReactiveValidationModuleConfigurationToken } from './reactive-validatio
2323
]
2424
})
2525
export class ReactiveValidationModule {
26-
static forRoot(configuration?: ReactiveValidationModuleConfiguration): ModuleWithProviders {
26+
static forRoot(configuration?: ReactiveValidationModuleConfiguration): ModuleWithProviders<ReactiveValidationModule> {
2727
return {
2828
ngModule: ReactiveValidationModule,
2929
providers: [{

angular-reactive-validation/src/validation-message/validation-message.component.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { TestBed, ComponentFixture } from '@angular/core/testing';
33

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

79
describe('ValidationMessageComponent', () => {
810
describe('canHandle', () => {
@@ -115,4 +117,36 @@ describe('ValidationMessageComponent', () => {
115117
}
116118
}
117119
});
120+
121+
it('can set control by name without exception being thrown due to ControlContainer not yet being initialized', () => {
122+
@Component({
123+
template: `
124+
<form [formGroup]="form">
125+
<arv-validation-message for="age" key="min">
126+
</arv-validation-message>
127+
</form>
128+
`
129+
})
130+
class TestHostComponent {
131+
age = new FormControl(0, [
132+
Validators.min(10, 'invalid age')
133+
]);
134+
form = new FormGroup({
135+
age: this.age
136+
});
137+
138+
@ViewChild(ValidationMessageComponent, { static: true }) validationMessageComponent: ValidationMessageComponent;
139+
}
140+
141+
TestBed.configureTestingModule({
142+
imports: [ReactiveFormsModule],
143+
declarations: [ValidationMessageComponent, TestHostComponent]
144+
});
145+
146+
expect(() => {
147+
const fixture = TestBed.createComponent(TestHostComponent);
148+
fixture.detectChanges();
149+
expect(fixture.componentInstance.validationMessageComponent.for).toBe(fixture.componentInstance.age);
150+
}).not.toThrow();
151+
});
118152
});

angular-reactive-validation/src/validation-message/validation-message.component.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Component, Input, ViewEncapsulation, Optional } from '@angular/core';
1+
import { Component, Input, ViewEncapsulation, Optional, OnInit } from '@angular/core';
22
import { FormControl, ValidationErrors, ControlContainer } from '@angular/forms';
33

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

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

@@ -27,6 +27,10 @@ export class ValidationMessageComponent {
2727
* ValidationMessagesComponent has multiple FormControls specified.
2828
*/
2929
set for(control: FormControl | string | undefined) {
30+
if (!isControlContainerVoidOrInitialized(this.controlContainer)) {
31+
this.initializeForOnInit = () => this.for = control;
32+
return;
33+
}
3034
this._for = typeof control === 'string' ? getFormControlFromContainer(control, this.controlContainer) : control;
3135
}
3236
get for(): FormControl | string | undefined {
@@ -39,6 +43,8 @@ export class ValidationMessageComponent {
3943
*/
4044
key: string | undefined;
4145

46+
private initializeForOnInit = () => {};
47+
4248
/**
4349
* The ValidationErrors object that contains contextual information about the error, which can be used for
4450
* displaying, e.g. the minimum length within the error message.
@@ -47,6 +53,10 @@ export class ValidationMessageComponent {
4753
return this._context;
4854
}
4955

56+
ngOnInit() {
57+
this.initializeForOnInit();
58+
}
59+
5060
canHandle(error: ValidationError) {
5161
return (!this.for || error.control === this.for) && error.key === this.key;
5262
}

angular-reactive-validation/src/validation-messages/validation-messages.component.spec.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Component } from '@angular/core';
1+
import { Component, ViewChild } from '@angular/core';
22
import { TestBed, ComponentFixture } from '@angular/core/testing';
3-
import { ControlContainer, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
3+
import { ControlContainer, FormGroup, FormControl, ReactiveFormsModule, FormGroupDirective } from '@angular/forms';
44
import { Subject } from 'rxjs';
55

66
import { ValidationMessagesComponent } from './validation-messages.component';
@@ -33,14 +33,13 @@ describe('ValidationMessagesComponent', () => {
3333
lastName: lastNameControl
3434
});
3535

36-
const controlContainer: any = {
37-
control: formGroup
38-
};
36+
const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []);
37+
formGroupDirective.form = formGroup;
3938

4039
TestBed.configureTestingModule({
4140
declarations: [ValidationMessagesComponent],
4241
providers: [
43-
{ provide: ControlContainer, useValue: controlContainer }
42+
{ provide: ControlContainer, useValue: formGroupDirective }
4443
]
4544
});
4645

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

243+
it('can set control by name without exception being thrown due to ControlContainer not yet being initialized', () => {
244+
@Component({
245+
template: `
246+
<form [formGroup]="form">
247+
<input type="number" formControlName="age">
248+
<arv-validation-messages for="age"></arv-validation-messages>
249+
</form>
250+
`
251+
})
252+
class TestHostComponent {
253+
age = new FormControl(0, [
254+
Validators.min(10, 'invalid age')
255+
]);
256+
form = new FormGroup({
257+
age: this.age
258+
});
259+
260+
@ViewChild(ValidationMessagesComponent, { static: true }) validationMessagesComponent: ValidationMessagesComponent;
261+
}
262+
263+
TestBed.configureTestingModule({
264+
imports: [ReactiveFormsModule],
265+
declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent]
266+
});
267+
268+
expect(() => {
269+
const fixture = TestBed.createComponent(TestHostComponent);
270+
fixture.componentInstance.age.markAsTouched();
271+
fixture.detectChanges();
272+
expect(fixture.componentInstance.validationMessagesComponent.getErrorMessages()).toEqual(['invalid age']);
273+
}).not.toThrow();
274+
});
275+
244276
xdescribe('', () => {
245277
let onerrorBeforeTest: OnErrorEventHandler;
246278
beforeEach(() => {

0 commit comments

Comments
 (0)