Skip to content

Commit a545bb9

Browse files
committed
feat: add generic form error messages
1 parent 0add550 commit a545bb9

File tree

3 files changed

+321
-78
lines changed

3 files changed

+321
-78
lines changed

Diff for: packages/angular/material-error/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"@angular/common": ">=14.1.0 < 18.0.0",
77
"@angular/core": ">=14.1.0 < 18.0.0",
88
"@angular/forms": ">=14.1.0 < 18.0.0",
9-
"@angular/material": ">=14.1.0 < 18.0.0",
109
"@angular/platform-browser": ">=14.1.0 < 18.0.0",
1110
"rxjs": ">=6.0.0 < 8.0.0"
1211
},
12+
"optionalDependencies": {
13+
"@angular/material": ">=14.1.0 < 18.0.0"
14+
},
1315
"dependencies": {
1416
"tslib": "^2.3.0"
1517
},

Diff for: packages/angular/material-error/src/lib/dynamic-error.directive.spec.ts

+286-70
Original file line numberDiff line numberDiff line change
@@ -5,96 +5,312 @@ import { MatFormFieldModule } from '@angular/material/form-field';
55
import { Component, ViewChild } from '@angular/core';
66
import {
77
FormControl,
8+
FormGroup,
9+
FormsModule,
810
ReactiveFormsModule,
911
ValidationErrors,
1012
Validators,
1113
} from '@angular/forms';
1214
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
1315
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
16+
import { fireEvent } from '@testing-library/dom';
1417
import { ErrorMessageProvider } from './error-message-provider';
1518
import { DynamicErrorDirective } from './dynamic-error.directive';
1619

17-
@Component({
18-
template: `<mat-form-field>
19-
<input matInput [formControl]="formControl" />
20-
<mat-error *schamanDynamicError></mat-error>
21-
</mat-form-field>`,
22-
standalone: true,
23-
imports: [
24-
DynamicErrorDirective,
25-
MatInputModule,
26-
MatFormFieldModule,
27-
ReactiveFormsModule,
28-
],
29-
})
30-
class DirectiveTestComponent {
31-
@ViewChild(DynamicErrorDirective, { static: true }) directive:
32-
| DynamicErrorDirective
33-
| undefined;
34-
public readonly formControl = new FormControl('', Validators.required);
35-
}
36-
37-
const render = async () => {
38-
await TestBed.configureTestingModule({
39-
imports: [DirectiveTestComponent, NoopAnimationsModule],
40-
providers: [
41-
{
42-
provide: ErrorMessageProvider,
43-
useValue: {
44-
getErrorMessagesFor: (error: ValidationErrors) =>
45-
Object.keys(error)[0],
46-
},
47-
},
48-
],
49-
}).compileComponents();
50-
51-
const fixture = TestBed.createComponent(DirectiveTestComponent);
52-
fixture.detectChanges();
53-
const loader = TestbedHarnessEnvironment.loader(fixture);
54-
return { fixture, loader };
55-
};
56-
5720
describe('DynamicErrorDirective', () => {
58-
it('should create', async () => {
59-
const { fixture } = await render();
60-
expect(fixture.componentInstance).toBeTruthy();
61-
});
21+
describe('Reactive form with material input', () => {
22+
@Component({
23+
template: `<form [formGroup]="formGroup">
24+
<mat-form-field>
25+
<input matInput [formControl]="formControl" />
26+
<mat-error *schamanDynamicError></mat-error>
27+
</mat-form-field>
28+
</form>`,
29+
standalone: true,
30+
imports: [
31+
DynamicErrorDirective,
32+
MatInputModule,
33+
MatFormFieldModule,
34+
ReactiveFormsModule,
35+
],
36+
})
37+
class DirectiveTestComponent {
38+
@ViewChild(DynamicErrorDirective, { static: true }) directive:
39+
| DynamicErrorDirective
40+
| undefined;
41+
public readonly formControl = new FormControl('', Validators.required);
42+
public readonly formGroup = new FormGroup({
43+
formControl: this.formControl,
44+
});
45+
}
46+
47+
const render = async () => {
48+
await TestBed.configureTestingModule({
49+
imports: [DirectiveTestComponent, NoopAnimationsModule],
50+
providers: [
51+
{
52+
provide: ErrorMessageProvider,
53+
useValue: {
54+
getErrorMessagesFor: (error: ValidationErrors) =>
55+
Object.keys(error)[0],
56+
},
57+
},
58+
],
59+
}).compileComponents();
60+
61+
const fixture = TestBed.createComponent(DirectiveTestComponent);
62+
fixture.detectChanges();
63+
const loader = TestbedHarnessEnvironment.loader(fixture);
64+
return { fixture, loader };
65+
};
66+
it('should create', async () => {
67+
const { fixture } = await render();
68+
expect(fixture.componentInstance).toBeTruthy();
69+
});
70+
71+
it('should show error message', async () => {
72+
const { fixture, loader } = await render();
73+
const input = await loader.getHarness(MatInputHarness);
74+
75+
await input.focus();
76+
await input.setValue('a');
77+
await input.setValue('');
78+
await input.blur();
79+
80+
expect(fixture.nativeElement.textContent).toContain('required');
81+
});
82+
83+
it('should hide error message', async () => {
84+
const { fixture, loader } = await render();
85+
const input = await loader.getHarness(MatInputHarness);
86+
87+
await input.focus();
88+
await input.setValue('a');
89+
await input.setValue('');
90+
await input.setValue('a');
91+
await input.blur();
92+
93+
expect(fixture.nativeElement.textContent).not.toContain('required');
94+
});
6295

63-
it('should show error message', async () => {
64-
const { fixture, loader } = await render();
65-
const input = await loader.getHarness(MatInputHarness);
96+
it('should unsubscribe in on destroy', async () => {
97+
const { fixture, loader } = await render();
98+
const input = await loader.getHarness(MatInputHarness);
6699

67-
await input.focus();
68-
await input.setValue('a');
69-
await input.setValue('');
70-
await input.blur();
100+
fixture.componentInstance.directive?.ngOnDestroy();
101+
await input.focus();
102+
await input.setValue('a');
103+
await input.setValue('');
104+
await input.blur();
71105

72-
expect(fixture.nativeElement.textContent).toContain('required');
106+
expect(fixture.nativeElement.textContent).not.toContain('required');
107+
});
108+
109+
it('should show error message on submit', async () => {
110+
const { fixture } = await render();
111+
112+
const form = fixture.nativeElement.querySelector('form');
113+
fireEvent.submit(form);
114+
115+
fixture.detectChanges();
116+
117+
expect(fixture.nativeElement.textContent).toContain('required');
118+
});
73119
});
74120

75-
it('should hide error message', async () => {
76-
const { fixture, loader } = await render();
77-
const input = await loader.getHarness(MatInputHarness);
121+
describe('Form with material input', () => {
122+
@Component({
123+
template: `<form>
124+
<mat-form-field>
125+
<input matInput [(ngModel)]="value" required name="test" />
126+
<mat-error *schamanDynamicError></mat-error>
127+
</mat-form-field>
128+
</form>`,
129+
standalone: true,
130+
imports: [
131+
DynamicErrorDirective,
132+
MatInputModule,
133+
MatFormFieldModule,
134+
FormsModule,
135+
],
136+
})
137+
class DirectiveTestComponent {
138+
@ViewChild(DynamicErrorDirective, { static: true }) directive:
139+
| DynamicErrorDirective
140+
| undefined;
141+
public value = '';
142+
}
143+
144+
const render = async () => {
145+
await TestBed.configureTestingModule({
146+
imports: [DirectiveTestComponent, NoopAnimationsModule],
147+
providers: [
148+
{
149+
provide: ErrorMessageProvider,
150+
useValue: {
151+
getErrorMessagesFor: (error: ValidationErrors) =>
152+
Object.keys(error)[0],
153+
},
154+
},
155+
],
156+
}).compileComponents();
157+
158+
const fixture = TestBed.createComponent(DirectiveTestComponent);
159+
fixture.detectChanges();
160+
const loader = TestbedHarnessEnvironment.loader(fixture);
161+
return { fixture, loader };
162+
};
163+
it('should create', async () => {
164+
const { fixture } = await render();
165+
expect(fixture.componentInstance).toBeTruthy();
166+
});
167+
168+
it('should show error message', async () => {
169+
const { fixture, loader } = await render();
170+
const input = await loader.getHarness(MatInputHarness);
171+
172+
await input.focus();
173+
await input.setValue('a');
174+
await input.setValue('');
175+
await input.blur();
176+
177+
expect(fixture.nativeElement.textContent).toContain('required');
178+
});
179+
180+
it('should hide error message', async () => {
181+
const { fixture, loader } = await render();
182+
const input = await loader.getHarness(MatInputHarness);
78183

79-
await input.focus();
80-
await input.setValue('a');
81-
await input.setValue('');
82-
await input.setValue('a');
83-
await input.blur();
184+
await input.focus();
185+
await input.setValue('a');
186+
await input.setValue('');
187+
await input.setValue('a');
188+
await input.blur();
84189

85-
expect(fixture.nativeElement.textContent).not.toContain('required');
190+
expect(fixture.nativeElement.textContent).not.toContain('required');
191+
});
192+
193+
it('should unsubscribe in on destroy', async () => {
194+
const { fixture, loader } = await render();
195+
const input = await loader.getHarness(MatInputHarness);
196+
197+
fixture.componentInstance.directive?.ngOnDestroy();
198+
await input.focus();
199+
await input.setValue('a');
200+
await input.setValue('');
201+
await input.blur();
202+
203+
expect(fixture.nativeElement.textContent).not.toContain('required');
204+
});
205+
206+
it('should show error message on submit', async () => {
207+
const { fixture } = await render();
208+
209+
const form = fixture.nativeElement.querySelector('form');
210+
fireEvent.submit(form);
211+
212+
fixture.detectChanges();
213+
214+
expect(fixture.nativeElement.textContent).toContain('required');
215+
});
86216
});
87217

88-
it('should unsubscribe in on destroy', async () => {
89-
const { fixture, loader } = await render();
90-
const input = await loader.getHarness(MatInputHarness);
218+
describe('Form control bare', () => {
219+
@Component({
220+
template: `<form [formGroup]="formGroup">
221+
<input [formControl]="formControl" />
222+
<div *schamanDynamicError="formControl"></div>
223+
</form>`,
224+
standalone: true,
225+
imports: [DynamicErrorDirective, ReactiveFormsModule],
226+
})
227+
class DirectiveTestComponent {
228+
@ViewChild(DynamicErrorDirective, { static: true }) directive:
229+
| DynamicErrorDirective
230+
| undefined;
231+
public readonly formControl = new FormControl('', Validators.required);
232+
public readonly formGroup = new FormGroup({
233+
formControl: this.formControl,
234+
});
235+
}
236+
237+
const render = async () => {
238+
await TestBed.configureTestingModule({
239+
imports: [DirectiveTestComponent, NoopAnimationsModule],
240+
providers: [
241+
{
242+
provide: ErrorMessageProvider,
243+
useValue: {
244+
getErrorMessagesFor: (error: ValidationErrors) =>
245+
Object.keys(error)[0],
246+
},
247+
},
248+
],
249+
}).compileComponents();
250+
251+
const fixture = TestBed.createComponent(DirectiveTestComponent);
252+
fixture.detectChanges();
253+
return { fixture };
254+
};
255+
it('should create', async () => {
256+
const { fixture } = await render();
257+
expect(fixture.componentInstance).toBeTruthy();
258+
});
259+
260+
it('should show error message', async () => {
261+
const { fixture } = await render();
262+
const input = fixture.nativeElement.querySelector('input');
263+
264+
fireEvent.focus(input);
265+
fireEvent.input(input, { target: { value: 'a' } });
266+
fixture.detectChanges();
267+
fireEvent.input(input, { target: { value: '' } });
268+
fireEvent.blur(input);
269+
fixture.detectChanges();
270+
271+
expect(fixture.nativeElement.textContent).toContain('required');
272+
});
273+
274+
it('should hide error message', async () => {
275+
const { fixture } = await render();
276+
const input = fixture.nativeElement.querySelector('input');
277+
278+
fireEvent.focus(input);
279+
fireEvent.input(input, { target: { value: 'a' } });
280+
fixture.detectChanges();
281+
fireEvent.input(input, { target: { value: '' } });
282+
fixture.detectChanges();
283+
fireEvent.input(input, { target: { value: 'a' } });
284+
fireEvent.blur(input);
285+
fixture.detectChanges();
286+
287+
expect(fixture.nativeElement.textContent).not.toContain('required');
288+
});
289+
290+
it('should unsubscribe in on destroy', async () => {
291+
const { fixture } = await render();
292+
const input = fixture.nativeElement.querySelector('input');
293+
294+
fixture.componentInstance.directive?.ngOnDestroy();
295+
fireEvent.focus(input);
296+
fireEvent.input(input, { target: { value: 'a' } });
297+
fixture.detectChanges();
298+
fireEvent.input(input, { target: { value: '' } });
299+
fireEvent.blur(input);
300+
fixture.detectChanges();
301+
302+
expect(fixture.nativeElement.textContent).not.toContain('required');
303+
});
304+
305+
it('should show error message on submit', async () => {
306+
const { fixture } = await render();
307+
308+
const form = fixture.nativeElement.querySelector('form');
309+
fireEvent.submit(form);
91310

92-
fixture.componentInstance.directive?.ngOnDestroy();
93-
await input.focus();
94-
await input.setValue('a');
95-
await input.setValue('');
96-
await input.blur();
311+
fixture.detectChanges();
97312

98-
expect(fixture.nativeElement.textContent).not.toContain('required');
313+
expect(fixture.nativeElement.textContent).toContain('required');
314+
});
99315
});
100316
});

0 commit comments

Comments
 (0)