Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { NgxControlError } from 'ngxtension/control-error';

<label>
<b>Name</b>
<input type="text" [formControl]="form.controls.name" />
<strong *ngxControlError="form.controls.name; track: 'required'">
<input type="text" formControlName="name" />
<strong *ngxControlError="'name'; track: 'required'">
Name is required.
</strong>
</label>
Expand Down
14 changes: 14 additions & 0 deletions docs/src/content/docs/es/utilities/Forms/control-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ Sin `NgxControlError`:
</label>
```

En un formulario también puedes pasar el nombre del control en lugar de la instancia.

```html
<form [formGroup]="form">
<label>
<b>Nombre</b>
<input type="text" formControlName="name" />
<strong *ngxControlError="'name'; track: 'required'">
Se requiere el nombre.
</strong>
</label>
</form>
```

## Configuración

Un `StateMatcher` define cuándo el control proporcionado está en un _estado de error_.
Expand Down
14 changes: 14 additions & 0 deletions docs/src/content/docs/utilities/Forms/control-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ without `NgxControlError`:
</label>
```

In a form you can also pass the name of the control instead of the instance.

```html
<form [formGroup]="form">
<label>
<b>Name</b>
<input type="text" formControlName="name" />
<strong *ngxControlError="'name'; track: 'required'">
Name is required.
</strong>
</label>
</form>
```

## Configuration

A `StateMatcher` defines when the provided control is in an _error state_.
Expand Down
183 changes: 169 additions & 14 deletions libs/ngxtension/control-error/src/control-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
import { TestBed } from '@angular/core/testing';
import {
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { By } from '@angular/platform-browser';
Expand All @@ -22,7 +24,7 @@ import {
} from './control-error';

describe('NgxControlError', () => {
const unitTest = (test: () => void | Promise<void>) => async () => {
const isolatedTest = (test: () => void | Promise<void>) => async () => {
TestBed.overrideProvider(TemplateRef, { useValue: undefined });
TestBed.overrideProvider(ViewContainerRef, {
useValue: {
Expand All @@ -39,13 +41,13 @@ describe('NgxControlError', () => {
await TestBed.runInInjectionContext(test);
};

const render = (
const render = <TInputs>(
template: string,
inputs?: Partial<NgxControlError> | undefined,
inputs?: Partial<TInputs> | undefined,
providers?: Provider[],
) => {
@Component({
imports: [CommonModule, NgxControlError],
imports: [CommonModule, ReactiveFormsModule, NgxControlError],
standalone: true,
template,
providers,
Expand All @@ -70,12 +72,12 @@ describe('NgxControlError', () => {

it(
'should be created',
unitTest(() => expect(new NgxControlError()).toBeTruthy()),
isolatedTest(() => expect(new NgxControlError()).toBeTruthy()),
);

it(
'should have a context guard',
unitTest(() =>
isolatedTest(() =>
expect(
NgxControlError.ngTemplateContextGuard(new NgxControlError(), {}),
).toBe(true),
Expand All @@ -85,7 +87,7 @@ describe('NgxControlError', () => {
describe('should have an error when the control includes the tracked error and the control is in an error state', () => {
it(
'respecting track changes',
unitTest(() => {
isolatedTest(() => {
const instance = new NgxControlError();

instance.track$.set('required');
Expand All @@ -104,7 +106,7 @@ describe('NgxControlError', () => {

it(
'when it has at least 1 tracked error ',
unitTest(() => {
isolatedTest(() => {
const instance = new NgxControlError();
const control = new FormControl('42', [
Validators.minLength(3),
Expand All @@ -128,7 +130,7 @@ describe('NgxControlError', () => {

it(
'respecting error state matcher changes',
unitTest(() => {
isolatedTest(() => {
const instance = new NgxControlError();

instance.track$.set('required');
Expand All @@ -147,7 +149,7 @@ describe('NgxControlError', () => {

it(
'respecting control instance changes',
unitTest(() => {
isolatedTest(() => {
const instance = new NgxControlError();

instance.track$.set('required');
Expand All @@ -167,7 +169,7 @@ describe('NgxControlError', () => {

it(
'DEFAULT_ERROR_STATE_MATCHER should match when the control is: 1. invalid 2. touched or its parent is submitted',
unitTest(() => {
isolatedTest(() => {
const instance = new NgxControlError();
const control = new FormControl('', Validators.required);

Expand Down Expand Up @@ -241,17 +243,30 @@ describe('NgxControlError', () => {
it('should have an injectable error state matcher', () => {
const errorStateMatcher = jest.fn();

const params = {
error: 'required',
control: new FormControl('', Validators.required),
};

const [, controlError] = render(
'<ng-template ngxControlError />',
undefined,
'<span *ngxControlError="control; track: error">42</span>',
params,
provideNgxControlError({ errorStateMatcher: () => errorStateMatcher }),
);

expect(controlError.errorStateMatcher$()).toBe(errorStateMatcher);
});

it('should use the default error state matcher as default', () => {
const [, controlError] = render('<ng-template ngxControlError />');
const params = {
error: 'required',
control: new FormControl('', Validators.required),
};

const [, controlError] = render(
'<span *ngxControlError="control; track: error">42</span>',
params,
);

expect(controlError.errorStateMatcher$()).toBe(
NGX_DEFAULT_CONTROL_ERROR_STATE_MATCHER,
Expand Down Expand Up @@ -299,4 +314,144 @@ describe('NgxControlError', () => {
'INVALID - true',
);
});

it('should resolve a control by its name', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hi @RobbyRabbitman
Can we add one more test with nested form group?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, do u know if the Token ControlContainer can be used to inject the parent control directive? From the source code it seems like those directives provide themselves see e.g. https://github.com/angular/angular/blob/f80b51a738ebccf5f280c50db85d0e250191a2a1/packages/forms/src/directives/reactive_directives/form_group_name.ts#L33C1-L36C3

const params = {
error: 'required',
form: new FormGroup({
name: new FormControl('', Validators.required),
}),
stateMatcher: () => of(true),
};

const [fixture, controlError] = render(
`<form [formGroup]="form">
<label>
<input formControlName="name" />
<span *ngxControlError="'name'; track: error, errorStateMatcher: stateMatcher">42</span>
</label>
</form>
`,
params,
);

fixture.detectChanges();

expect(fixture.debugElement.nativeElement.textContent).toBe('42');

controlError.errorStateMatcher = () => of(false);

fixture.detectChanges();

expect(fixture.debugElement.nativeElement.textContent).toBe('');
});

it('should throw when a control cannot be found because there is no parent control', () => {
expect(() =>
render(`
<span *ngxControlError="'name'; track: 'required'">42</span>
`),
).toThrow(
'[NgxControlError]: A control name cannot be specified without a parent FormGroup.',
);
});

it('should throw when a control cannot be found in the parent form group', () => {
const params = {
form: new FormGroup({
name: new FormControl('', Validators.required),
}),
};

expect(() =>
render(
`<form [formGroup]="form">
<span *ngxControlError="'nonExistentControlname'; track: 'required'">42</span>
</form>
`,
params,
),
).toThrow(
`[NgxControlError]: Cannot find control with name 'nonExistentControlname'.`,
);
});

it('should throw when a control cannot be found in a nested parent form group', () => {
expect(() =>
render(
`<form [formGroup]="form">
<div formGroupName="nested">
<span *ngxControlError="'nonExistentControlname'; track: 'required'">42</span>
</div>
</form>
`,
{
form: new FormGroup({
nested: new FormGroup({
name: new FormControl('', Validators.required),
}),
}),
},
),
).toThrow(
`[NgxControlError]: Cannot find control with name 'nonExistentControlname'.`,
);

expect(() =>
render(
`<form [formGroup]="form">
<div formGroupName="nested">
<span *ngxControlError="'name1'; track: 'required'">42</span>
</div>
</form>
`,
{
form: new FormGroup({
name1: new FormControl('', Validators.required),
nested: new FormGroup({
name2: new FormControl('', Validators.required),
}),
}),
},
),
).toThrow(`[NgxControlError]: Cannot find control with name 'name1'.`);
});

it('should resolve a nested control by its name', () => {
const params = {
error: 'required',
form: new FormGroup({
nested: new FormGroup({
name: new FormControl('', Validators.required),
}),
}),
stateMatcher: () => of(true),
};

const [fixture, controlError] = render(
`
<form [formGroup]="form">
<div formGroupName="nested">
<label>
<input formControlName="name" />
<span
*ngxControlError="'name'; track: error, errorStateMatcher: stateMatcher"
>42</span>
</label>
</div>
</form>
`,
params,
);

fixture.detectChanges();

expect(fixture.debugElement.nativeElement.textContent).toBe('42');

controlError.errorStateMatcher = () => of(false);

fixture.detectChanges();

expect(fixture.debugElement.nativeElement.textContent).toBe('');
});
});
Loading
Loading