Skip to content

Commit

Permalink
fix(forms): Allow NonNullableFormBuilder to be injected. (#45904)
Browse files Browse the repository at this point in the history
Based on early feedback, calling `fb.nonNullable.group(...)` continues to be clunky for a form with many such groups. Allowing `NonNullableFormBuilder` to be directly injected enables the following:

```
constructor(private fb: NonNullableFormBuilder) {}
```

PR Close #45904
  • Loading branch information
dylhunn authored and AndrewKushnir committed May 10, 2022
1 parent 216a966 commit 43ba4ab
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 11 deletions.
12 changes: 8 additions & 4 deletions goldens/public-api/forms/index.md
Expand Up @@ -706,12 +706,16 @@ export class NgSelectOption implements OnDestroy {
}

// @public
export interface NonNullableFormBuilder {
array<T>(controls: Array<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArrayElement<T, never>>;
control<T>(formState: T | FormControlState<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl<T>;
group<T extends {}>(controls: T, options?: AbstractControlOptions | null): FormGroup<{
export abstract class NonNullableFormBuilder {
abstract array<T>(controls: Array<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArrayElement<T, never>>;
abstract control<T>(formState: T | FormControlState<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl<T>;
abstract group<T extends {}>(controls: T, options?: AbstractControlOptions | null): FormGroup<{
[K in keyof T]: ɵElement<T[K], never>;
}>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<NonNullableFormBuilder, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<NonNullableFormBuilder>;
}

// @public
Expand Down
14 changes: 9 additions & 5 deletions packages/forms/src/form_builder.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Injectable} from '@angular/core';
import {inject, Injectable, InjectionToken} from '@angular/core';

import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {ReactiveFormsModule} from './form_providers';
Expand Down Expand Up @@ -315,13 +315,17 @@ export class FormBuilder {
*
* @publicApi
*/
export interface NonNullableFormBuilder {
@Injectable({
providedIn: ReactiveFormsModule,
useFactory: () => inject(FormBuilder).nonNullable,
})
export abstract class NonNullableFormBuilder {
/**
* Similar to {@see FormBuilder#group}, except any implicitly constructed `FormControl`
* will be non-nullable (i.e. it will have `initialValueIsDefault` set to true). Note
* that already-constructed controls will not be altered.
*/
group<T extends {}>(
abstract group<T extends {}>(
controls: T,
options?: AbstractControlOptions|null,
): FormGroup<{[K in keyof T]: ɵElement<T[K], never>}>;
Expand All @@ -331,15 +335,15 @@ export interface NonNullableFormBuilder {
* will be non-nullable (i.e. it will have `initialValueIsDefault` set to true). Note
* that already-constructed controls will not be altered.
*/
array<T>(
abstract array<T>(
controls: Array<T>, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T, never>>;

/**
* Similar to {@see FormBuilder#control}, except this overridden version of `control` forces
* `initialValueIsDefault` to be `true`, resulting in the control always being non-nullable.
*/
control<T>(
abstract control<T>(
formState: T|FormControlState<T>,
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl<T>;
Expand Down
59 changes: 57 additions & 2 deletions packages/forms/test/form_builder_spec.ts
Expand Up @@ -5,8 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {fakeAsync, tick} from '@angular/core/testing';
import {FormBuilder, UntypedFormBuilder, Validators} from '@angular/forms';
import {Component} from '@angular/core';
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
import {FormBuilder, NonNullableFormBuilder, ReactiveFormsModule, UntypedFormBuilder, Validators} from '@angular/forms';
import {of} from 'rxjs';

(function() {
Expand Down Expand Up @@ -197,6 +198,60 @@ describe('Form Builder', () => {
expect(a.errors).toEqual({'sync1': true, 'sync2': true});
});

it('should be injectable', () => {
@Component({
standalone: true,
template: '...',
})
class MyComp {
constructor(public fb: FormBuilder) {}
}

TestBed.configureTestingModule({imports: [ReactiveFormsModule]});
const fixture = TestBed.createComponent(MyComp);

fixture.detectChanges();
expect(fixture.componentInstance.fb).toBeInstanceOf(FormBuilder);

const fc = fixture.componentInstance.fb.control('foo');
{
// Check the type of the value by assigning in each direction
type ValueType = string|null;
let t: ValueType = fc.value;
let t1 = fc.value;
t1 = null as unknown as ValueType;
}
fc.reset();
expect(fc.value).toEqual(null);
});

it('should be injectable as NonNullableFormBuilder', () => {
@Component({
standalone: true,
template: '...',
})
class MyComp {
constructor(public fb: NonNullableFormBuilder) {}
}

TestBed.configureTestingModule({imports: [ReactiveFormsModule]});

const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(fixture.componentInstance.fb).toBeInstanceOf(FormBuilder);

const fc = fixture.componentInstance.fb.control('foo');
{
// Check the type of the value by assigning in each direction
type ValueType = string;
let t: ValueType = fc.value;
let t1 = fc.value;
t1 = null as unknown as ValueType;
}
fc.reset();
expect(fc.value).toEqual('foo');
});

describe('updateOn', () => {
it('should default to on change', () => {
const c = b.control('');
Expand Down

0 comments on commit 43ba4ab

Please sign in to comment.