Skip to content

feat: Update to Angular 14 #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ testem.log
Thumbs.db

TESTS-*.xml
/.angular/cache/*
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 8.0.0

* add support for Angular 14
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Note that `formSubmitted` can be undefined when it's not known if the form is su

Angular provides a limited set of validator functions. To declare your own validator functions _and_ combine it with this library use the `ValidatorDeclaration` class. It supports declaring validators with zero, one or two arguments.

**Note** that if your validator doesn't return an object as the inner error result, but e.g. a `boolean` such as in the examples below, then this will be replaced by an object that can hold the validation message. Thus in the first example below `{ 'hasvalue': true }` becomes `{ 'hasvalue': { 'message': 'validation message' } }`.
**Note** that if your validator doesn't return an object as the inner error result, but e.g. a `boolean` such as in the examples below, then this will be replaced by an object that can hold the validation message. Thus, in the first example below `{ 'hasvalue': true }` becomes `{ 'hasvalue': { 'message': 'validation message' } }`.

```ts
const hasValueValidator = ValidatorDeclaration.wrapNoArgumentValidator(control => {
Expand Down
8 changes: 8 additions & 0 deletions angular-reactive-validation/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "../dist/angular-reactive-validation",
"lib": {
"entryFile": "src/public_api.ts"
},
"allowedNonPeerDependencies": ["."]
}
20 changes: 5 additions & 15 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": "7.0.0",
"version": "8.0.0",
"repository": {
"type": "git",
"url": "https://github.com/davidwalschots/angular-reactive-validation.git"
Expand All @@ -20,19 +20,9 @@
"private": false,
"dependencies": {},
"peerDependencies": {
"@angular/core": "^13.0.0",
"@angular/common": "^13.0.0",
"@angular/forms": "^13.0.0",
"rxjs": "^6.5.3"
},
"ngPackage": {
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "../dist/angular-reactive-validation",
"lib": {
"entryFile": "src/public_api.ts"
},
"allowedNonPeerDependencies": [
"."
]
"@angular/core": "^14.0.0",
"@angular/common": "^14.0.0",
"@angular/forms": "^14.0.0",
"rxjs": "^6.6.7"
}
}
4 changes: 2 additions & 2 deletions angular-reactive-validation/src/form/form.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { UntypedFormGroup, ReactiveFormsModule } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

Expand Down Expand Up @@ -37,6 +37,6 @@ describe('FormDirective', () => {
</form>`
})
class TestHostComponent {
form = new FormGroup({});
form = new UntypedFormGroup({});
}
});
14 changes: 7 additions & 7 deletions angular-reactive-validation/src/get-control-path.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { TestBed, inject } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import { UntypedFormBuilder } from '@angular/forms';

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

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

it(`emits paths for form groups`, inject([FormBuilder], (fb: FormBuilder) => {
it(`emits paths for form groups`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
const firstName = fb.control('');
fb.group({
name: fb.group({
Expand All @@ -23,7 +23,7 @@ describe('getControlPath', () => {
expect(getControlPath(firstName)).toEqual('name.firstName');
}));

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

Expand All @@ -42,21 +42,21 @@ describe('getControlPath', () => {
expect(getControlPath(firstName2)).toEqual('persons.1.firstName');
}));

it(`emits an empty string for a control without parents`, inject([FormBuilder], (fb: FormBuilder) => {
it(`emits an empty string for a control without parents`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
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) => {
it(`emits an index string for a control with only a form array as parent`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
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) => {
it(`emits a single identifier for a control with only a single form group as parent`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
const control = fb.control('');

fb.group({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { TestBed, inject } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import { UntypedFormBuilder } from '@angular/forms';

import { getFormControlFromContainer } from './get-form-control-from-container';

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

it(`gets a FormControl from the FormGroup`, inject([FormBuilder], (fb: FormBuilder) => {
it(`gets a FormControl from the FormGroup`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
const firstName = fb.control('');
const group = fb.group({
firstName: firstName
Expand All @@ -25,13 +25,13 @@ describe('getFormControlFromContainer', () => {
expect(getFormControlFromContainer('firstName', container)).toBe(firstName);
}));

it(`throws an Error when no container is provided`, inject([FormBuilder], (fb: FormBuilder) => {
it(`throws an Error when no container is provided`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
expect(() => getFormControlFromContainer('firstName', undefined)).toThrow(new Error(
`You can't pass a string to arv-validation-messages's for attribute, when the ` +
`arv-validation-messages element is not a child of an element with a formGroupName or formGroup declaration.`));
}));

it(`throws an Error when there is no FormControl with the given name`, inject([FormBuilder], (fb: FormBuilder) => {
it(`throws an Error when there is no FormControl with the given name`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
const group = fb.group({});

const container: any = {
Expand All @@ -44,7 +44,7 @@ describe('getFormControlFromContainer', () => {
));
}));

it(`throws an Error when there is a FormGroup with the given name`, inject([FormBuilder], (fb: FormBuilder) => {
it(`throws an Error when there is a FormGroup with the given name`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
const group = fb.group({
name: fb.group({})
});
Expand All @@ -59,7 +59,7 @@ describe('getFormControlFromContainer', () => {
));
}));

it(`throws an Error when there is a FormArray with the given name`, inject([FormBuilder], (fb: FormBuilder) => {
it(`throws an Error when there is a FormArray with the given name`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => {
const group = fb.group({
name: fb.array([])
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FormGroup, FormControl, ControlContainer, FormGroupDirective } from '@angular/forms';
import { UntypedFormGroup, UntypedFormControl, ControlContainer, FormGroupDirective } from '@angular/forms';

export const getFormControlFromContainer = (name: string, controlContainer: ControlContainer | undefined): FormControl => {
export const getFormControlFromContainer = (name: string, controlContainer: ControlContainer | undefined): UntypedFormControl => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we want to support typed forms as well? I'm not sure if this change handles both typed and untyped forms. It might be a good idea to add some typed form tests and check.

The Angular documentation lists an UntypedFormControl as an alias for FormControl<any>, but I'm not sure what the runtime behaviour of it all would be.

Copy link
Contributor Author

@jafin jafin Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the knowledge on this one at the moment, I honestly just did the bare minimum for the upgrade and my understanding was that the untyped maintained compatibility without any changes. Additional tests would be good yes.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In src\app\app.component.ts I changed UntypedFormBuilder to FormBuilder. This still works fine.

I suspect this is what happens:

  • type is a TypeScript construct which needs to be compiled away. Thus type UntypedFormControl = FormControl<any>; becomes FormControl<any>.
  • Generics are also a TypeScript construct which needs to be compiled away. Thus FormControl<any> becomes FormControl.
  • A typed form control would have say type FormControl<string | null>, which then would also become FormControl.

Thus a line like if (!(control instanceof UntypedFormControl)) { actually compiles to if (!(control instanceof FormControl)) {.

I'll release a new version somewhere later today or tomorrow.

if (controlContainer) {
const control = (controlContainer.control as FormGroup).controls[name];
const control = (controlContainer.control as UntypedFormGroup).controls[name];
if (!control) {
throw new Error(`There is no control named '${name}'` +
(getPath(controlContainer).length > 0 ? ` within '${getPath(controlContainer).join('.')}'` : '') + '.');
}
if (!(control instanceof FormControl)) {
if (!(control instanceof UntypedFormControl)) {
throw new Error(`The control named '${name}' ` +
(getPath(controlContainer).length > 0 ? `within '${getPath(controlContainer).join('.')}' ` : '') +
`is not a FormControl. Maybe you accidentally referenced a FormGroup or FormArray?`);
Expand Down
14 changes: 7 additions & 7 deletions angular-reactive-validation/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export {ReactiveValidationModule} from './reactive-validation.module';
export {ReactiveValidationModuleConfiguration} from './reactive-validation-module-configuration';
export {Validators} from './validators';
export {ValidatorDeclaration} from './validator-declaration';
export {ValidationMessageComponent} from './validation-message/validation-message.component';
export {FormDirective} from './form/form.directive';
export {ValidationMessagesComponent} from './validation-messages/validation-messages.component';
export { FormDirective } from './form/form.directive';
export { ReactiveValidationModule } from './reactive-validation.module';
export { ReactiveValidationModuleConfiguration } from './reactive-validation-module-configuration';
export { Validators } from './validators';
export { ValidatorDeclaration } from './validator-declaration';
export { ValidationMessagesComponent } from './validation-messages/validation-messages.component';
export { ValidationMessageComponent } from './validation-message/validation-message.component';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormControl } from '@angular/forms';
import { UntypedFormControl } from '@angular/forms';

export interface ReactiveValidationModuleConfiguration {
/**
Expand All @@ -9,5 +9,5 @@ export interface ReactiveValidationModuleConfiguration {
* @param formSubmitted whether the form is submitted or not. When undefined, it's not known
* if the form is submitted, due to the form tag missing a formGroup.
*/
displayValidationMessageWhen?: (control: FormControl, formSubmitted: boolean | undefined) => boolean;
displayValidationMessageWhen?: (control: UntypedFormControl, formSubmitted: boolean | undefined) => boolean;
}
8 changes: 4 additions & 4 deletions angular-reactive-validation/src/validation-error.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { FormControl, ValidationErrors } from '@angular/forms';
import { UntypedFormControl, ValidationErrors } from '@angular/forms';

export class ValidationError {
control: FormControl;
control: UntypedFormControl;
key: string;
errorObject: ValidationErrors;

constructor(control: FormControl, key: string, errorObject: ValidationErrors) {
constructor(control: UntypedFormControl, key: string, errorObject: ValidationErrors) {
this.control = control;
this.key = key;
this.errorObject = errorObject;
}

static fromFirstError(control: FormControl): ValidationError | undefined {
static fromFirstError(control: UntypedFormControl): ValidationError | undefined {
if (!control.errors) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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';
import { UntypedFormControl, UntypedFormGroup, ReactiveFormsModule } from '@angular/forms';

describe('ValidationMessageComponent', () => {
describe('canHandle', () => {
Expand Down Expand Up @@ -130,10 +130,10 @@ describe('ValidationMessageComponent', () => {
class TestHostComponent {
@ViewChild(ValidationMessageComponent, { static: true }) validationMessageComponent: ValidationMessageComponent;

age = new FormControl(0, [
age = new UntypedFormControl(0, [
Validators.min(10, 'invalid age')
]);
form = new FormGroup({
form = new UntypedFormGroup({
age: this.age
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Input, ViewEncapsulation, Optional, OnInit } from '@angular/core';
import { FormControl, ValidationErrors, ControlContainer } from '@angular/forms';
import { UntypedFormControl, ValidationErrors, ControlContainer } from '@angular/forms';

import { ValidationError } from '../validation-error';
import { getFormControlFromContainer, isControlContainerVoidOrInitialized } from '../get-form-control-from-container';
Expand All @@ -22,14 +22,14 @@ export class ValidationMessageComponent implements OnInit {
* The FormControl for which a custom validation message should be shown. This is only required when the parent
* ValidationMessagesComponent has multiple FormControls specified.
*/
set for(control: FormControl | string | undefined) {
set for(control: UntypedFormControl | 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 {
get for(): UntypedFormControl | string | undefined {
return this._for;
}

Expand All @@ -40,7 +40,7 @@ export class ValidationMessageComponent implements OnInit {
key: string | undefined;

private _context: ValidationErrors | undefined;
private _for: FormControl | undefined;
private _for: UntypedFormControl | undefined;

constructor(@Optional() private controlContainer: ControlContainer) { }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, ViewChild } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ControlContainer, FormGroup, FormControl, ReactiveFormsModule, FormGroupDirective } from '@angular/forms';
import { ControlContainer, UntypedFormGroup, ReactiveFormsModule, FormGroupDirective, FormControl, FormGroup, UntypedFormControl }
from '@angular/forms';
import { Subject } from 'rxjs';

import { ValidationMessagesComponent } from './validation-messages.component';
Expand All @@ -14,7 +15,7 @@ const isErrorEvent = (event: Event | string): event is ErrorEvent => (event as E
describe('ValidationMessagesComponent', () => {
describe('properties and functions', () => {
let component: ValidationMessagesComponent;
let formGroup: FormGroup;
let formGroup: UntypedFormGroup;
let firstNameControl: FormControl;
let middleNameControl: FormControl;
let lastNameControl: FormControl;
Expand All @@ -29,7 +30,7 @@ describe('ValidationMessagesComponent', () => {
Validators.required('A last name is required'),
Validators.minLength(5, minLength => `Last name needs to be at least ${minLength} characters long`)
]);
formGroup = new FormGroup({
formGroup = new UntypedFormGroup({
firstName: firstNameControl,
middleName: middleNameControl,
lastName: lastNameControl
Expand Down Expand Up @@ -83,7 +84,7 @@ describe('ValidationMessagesComponent', () => {
it(`getErrorMessages returns the first error message per touched control (default configuration)`, () => {
component.for = [firstNameControl, middleNameControl, lastNameControl];
firstNameControl.markAsTouched();
// We skip middleNameControl on purpose, to ensure that it doesn't return it's error.
// We skip middleNameControl on purpose, to ensure that it doesn't return its error.
lastNameControl.markAsTouched();
lastNameControl.setValue('abc');

Expand Down Expand Up @@ -131,7 +132,7 @@ describe('ValidationMessagesComponent', () => {

describe('an alternative configuration', () => {
const configuration = {
displayValidationMessageWhen: () => true
displayValidationMessageWhen: (_: UntypedFormControl, __: boolean | undefined) => true
};

beforeEach(() => {
Expand All @@ -155,7 +156,7 @@ describe('ValidationMessagesComponent', () => {

it(`displayValidationMessageWhen's formSubmitted is undefined when a FormDirective is not provided`, () => {
fixture.detectChanges();
expect(configuration.displayValidationMessageWhen).toHaveBeenCalledWith(jasmine.any(FormControl), undefined);
expect(configuration.displayValidationMessageWhen).toHaveBeenCalledWith(jasmine.any(UntypedFormControl), undefined);
});
});

Expand Down
Loading