Skip to content

Commit

Permalink
feat: allow throwing on missing control
Browse files Browse the repository at this point in the history
This adds a configuration option called `shouldThrowOnMissingControl` that checks if the control is not found, if set to a function that returns true.
It is set to a function that returns false by default, so this is not breaking change.

This allows to catch situations where the controlName has been wrongly specified:

```html
<input id="firstName" name="firstName" [(ngModel)]="user.firstName" #firstNameCtrl="ngModel" required/>
<!-- the control name mentions lastName whereas the control is firstName -->
<val-errors controlName="lastName" id="firstNameErrors">
```

In that case, if the new option is enabled, valdemort will throw:

```
ngx-valdemort: no control found for controlName: 'lastName'.
```

As the option accepts a function, it can easily be enabled in dev and tests, but disabled in production:

```
config.shouldThrowOnMissingControl = () => !environment.production;
```
  • Loading branch information
cexbrayat committed Mar 26, 2021
1 parent b8f582f commit c2b739b
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 1 deletion.
14 changes: 14 additions & 0 deletions projects/ngx-valdemort/src/lib/valdemort-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,18 @@ export class ValdemortConfig {
control: AbstractControl,
form: NgForm | FormGroupDirective | undefined
) => control.touched || (!!form && form.submitted);

/**
* Specifies if the library should throw an error when a control is not found.
* For example, this can happen if a typo was made in the `controlName`.
* If the check is enabled, then an error will be thrown in such a case.
* Otherwise, the error is silently ignored.
*
* The default value of this function returns false, thus disabling the check.
*
* You can enable the check by giving it a function that returns true,
* or you can enable it only in development for example with:
* `config.shouldThrowOnMissingControl = () => !environment.production`
*/
shouldThrowOnMissingControl: () => boolean = () => false;
}
67 changes: 67 additions & 0 deletions projects/ngx-valdemort/src/lib/validation-errors.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,47 @@ class TemplateDrivenComponentTester extends ComponentTester<TemplateDrivenTestCo
}
}

@Component({
selector: 'val-wrong-control-name-test',
template: `
<form (ngSubmit)="submit()">
<input id="firstName" name="firstName" [(ngModel)]="user.firstName" #firstNameCtrl="ngModel" required />
<!-- the control name mentions lastName whereas the control is firstName -->
<val-errors controlName="lastName" id="firstNameErrors">
<ng-template valError="required">first name required</ng-template>
</val-errors>
<button id="submit">Submit</button>
</form>
`
})
class WrongControlNameTestComponent {
user = {
firstName: ''
};

// eslint-disable-next-line @typescript-eslint/no-empty-function
submit() {}
}

class WrongControlNameComponentTester extends ComponentTester<WrongControlNameTestComponent> {
constructor() {
super(WrongControlNameTestComponent);
}

get firstName() {
return this.input('#firstName');
}

get firstNameErrors() {
return this.element('#firstNameErrors');
}

get submit() {
return this.button('#submit');
}
}

describe('ValidationErrorsComponent', () => {
beforeEach(() => jasmine.addMatchers(speculoosMatchers));

Expand Down Expand Up @@ -421,6 +462,32 @@ describe('ValidationErrorsComponent', () => {
});
});

describe('with wrong control name', () => {
let tester: WrongControlNameComponentTester;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [FormsModule, ValdemortModule],
declarations: [WrongControlNameTestComponent]
});

tester = new WrongControlNameComponentTester();
tick();
tester.detectChanges();
}));

it('should not throw by default', fakeAsync(() => {
expect(() => tester.detectChanges()).not.toThrowError();
expect(tester.firstNameErrors).not.toContainText('first name required');
}));

it('should throw if configured to', fakeAsync(() => {
const config = TestBed.inject(ValdemortConfig);
config.shouldThrowOnMissingControl = () => true;
expect(() => tester.detectChanges()).toThrowError(`ngx-valdemort: no control found for controlName: 'lastName'.`);
}));
});

describe('configuration', () => {
let tester: ReactiveComponentTester;

Expand Down
13 changes: 12 additions & 1 deletion projects/ngx-valdemort/src/lib/validation-errors.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,18 @@ export class ValidationErrorsComponent {
if (this.control) {
return this.control;
} else if ((this.controlName || (this.controlName as number) === 0) && (this.controlContainer.control as FormArray)?.controls) {
return (this.controlContainer.control as FormArray).controls[this.controlName as number];
const control = (this.controlContainer.control as FormArray).controls[this.controlName as number];
if (this.config.shouldThrowOnMissingControl()) {
// if the control is null, then there are two cases:
// - we are in a template driven form, and the controls might not be initialized yet
// - there was an error in the control name. If so, let's throw an error to help developers
// to avoid false positive in template driven forms, we check if the controls are initialized
// by checking if the `controls` object or array has any element
if (!control && Object.keys((this.controlContainer.control as FormArray)?.controls).length > 0) {
throw new Error(`ngx-valdemort: no control found for controlName: '${this.controlName}'.`);
}
}
return control;
}
return null;
}
Expand Down

0 comments on commit c2b739b

Please sign in to comment.