Skip to content

Commit ed6a25e

Browse files
robert hasemannDafnik
authored andcommitted
inject direct parent
1 parent 70bdefa commit ed6a25e

File tree

2 files changed

+160
-17
lines changed

2 files changed

+160
-17
lines changed

libs/ngxtension/control-error/src/control-error.spec.ts

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from './control-error';
2525

2626
describe('NgxControlError', () => {
27-
const unitTest = (test: () => void | Promise<void>) => async () => {
27+
const isolatedTest = (test: () => void | Promise<void>) => async () => {
2828
TestBed.overrideProvider(TemplateRef, { useValue: undefined });
2929
TestBed.overrideProvider(ViewContainerRef, {
3030
useValue: {
@@ -72,12 +72,12 @@ describe('NgxControlError', () => {
7272

7373
it(
7474
'should be created',
75-
unitTest(() => expect(new NgxControlError()).toBeTruthy()),
75+
isolatedTest(() => expect(new NgxControlError()).toBeTruthy()),
7676
);
7777

7878
it(
7979
'should have a context guard',
80-
unitTest(() =>
80+
isolatedTest(() =>
8181
expect(
8282
NgxControlError.ngTemplateContextGuard(new NgxControlError(), {}),
8383
).toBe(true),
@@ -87,7 +87,7 @@ describe('NgxControlError', () => {
8787
describe('should have an error when the control includes the tracked error and the control is in an error state', () => {
8888
it(
8989
'respecting track changes',
90-
unitTest(() => {
90+
isolatedTest(() => {
9191
const instance = new NgxControlError();
9292

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

107107
it(
108108
'when it has at least 1 tracked error ',
109-
unitTest(() => {
109+
isolatedTest(() => {
110110
const instance = new NgxControlError();
111111
const control = new FormControl('42', [
112112
Validators.minLength(3),
@@ -130,7 +130,7 @@ describe('NgxControlError', () => {
130130

131131
it(
132132
'respecting error state matcher changes',
133-
unitTest(() => {
133+
isolatedTest(() => {
134134
const instance = new NgxControlError();
135135

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

150150
it(
151151
'respecting control instance changes',
152-
unitTest(() => {
152+
isolatedTest(() => {
153153
const instance = new NgxControlError();
154154

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

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

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

246+
const params = {
247+
error: 'required',
248+
control: new FormControl('', Validators.required),
249+
};
250+
246251
const [, controlError] = render(
247-
'<ng-template ngxControlError />',
248-
undefined,
252+
'<span *ngxControlError="control; track: error">42</span>',
253+
params,
249254
provideNgxControlError({ errorStateMatcher: () => errorStateMatcher }),
250255
);
251256

252257
expect(controlError.errorStateMatcher$()).toBe(errorStateMatcher);
253258
});
254259

255260
it('should use the default error state matcher as default', () => {
256-
const [, controlError] = render('<ng-template ngxControlError />');
261+
const params = {
262+
error: 'required',
263+
control: new FormControl('', Validators.required),
264+
};
265+
266+
const [, controlError] = render(
267+
'<span *ngxControlError="control; track: error">42</span>',
268+
params,
269+
);
257270

258271
expect(controlError.errorStateMatcher$()).toBe(
259272
NGX_DEFAULT_CONTROL_ERROR_STATE_MATCHER,
@@ -332,4 +345,113 @@ describe('NgxControlError', () => {
332345

333346
expect(fixture.debugElement.nativeElement.textContent).toBe('');
334347
});
348+
349+
it('should throw when a control cannot be found because there is no parent control', () => {
350+
expect(() =>
351+
render(`
352+
<span *ngxControlError="'name'; track: 'required'">42</span>
353+
`),
354+
).toThrow(
355+
'[NgxControlError]: A control name cannot be specified without a parent FormGroup.',
356+
);
357+
});
358+
359+
it('should throw when a control cannot be found in the parent form group', () => {
360+
const params = {
361+
form: new FormGroup({
362+
name: new FormControl('', Validators.required),
363+
}),
364+
};
365+
366+
expect(() =>
367+
render(
368+
`<form [formGroup]="form">
369+
<span *ngxControlError="'nonExistentControlname'; track: 'required'">42</span>
370+
</form>
371+
`,
372+
params,
373+
),
374+
).toThrow(
375+
`[NgxControlError]: Cannot find control with name 'nonExistentControlname'.`,
376+
);
377+
});
378+
379+
it('should throw when a control cannot be found in a nested parent form group', () => {
380+
expect(() =>
381+
render(
382+
`<form [formGroup]="form">
383+
<div formGroupName="nested">
384+
<span *ngxControlError="'nonExistentControlname'; track: 'required'">42</span>
385+
</div>
386+
</form>
387+
`,
388+
{
389+
form: new FormGroup({
390+
nested: new FormGroup({
391+
name: new FormControl('', Validators.required),
392+
}),
393+
}),
394+
},
395+
),
396+
).toThrow(
397+
`[NgxControlError]: Cannot find control with name 'nonExistentControlname'.`,
398+
);
399+
400+
expect(() =>
401+
render(
402+
`<form [formGroup]="form">
403+
<div formGroupName="nested">
404+
<span *ngxControlError="'name1'; track: 'required'">42</span>
405+
</div>
406+
</form>
407+
`,
408+
{
409+
form: new FormGroup({
410+
name1: new FormControl('', Validators.required),
411+
nested: new FormGroup({
412+
name2: new FormControl('', Validators.required),
413+
}),
414+
}),
415+
},
416+
),
417+
).toThrow(`[NgxControlError]: Cannot find control with name 'name1'.`);
418+
});
419+
420+
it('should resolve a nested control by its name', () => {
421+
const params = {
422+
error: 'required',
423+
form: new FormGroup({
424+
nested: new FormGroup({
425+
name: new FormControl('', Validators.required),
426+
}),
427+
}),
428+
stateMatcher: () => of(true),
429+
};
430+
431+
const [fixture, controlError] = render(
432+
`
433+
<form [formGroup]="form">
434+
<div formGroupName="nested">
435+
<label>
436+
<input formControlName="name" />
437+
<span
438+
*ngxControlError="'name'; track: error, errorStateMatcher: stateMatcher"
439+
>42</span>
440+
</label>
441+
</div>
442+
</form>
443+
`,
444+
params,
445+
);
446+
447+
fixture.detectChanges();
448+
449+
expect(fixture.debugElement.nativeElement.textContent).toBe('42');
450+
451+
controlError.errorStateMatcher = () => of(false);
452+
453+
fixture.detectChanges();
454+
455+
expect(fixture.debugElement.nativeElement.textContent).toBe('');
456+
});
335457
});

libs/ngxtension/control-error/src/control-error.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '@angular/core/rxjs-interop';
1818
import {
1919
AbstractControl,
20+
ControlContainer,
2021
FormGroupDirective,
2122
NgForm,
2223
type ValidationErrors,
@@ -299,10 +300,23 @@ export class NgxControlError {
299300
return;
300301
}
301302

302-
/**
303-
* TODO: throw an error if the control is not found?
304-
*/
305-
this.control$.set(this.parent$()?.control.get(control) ?? undefined);
303+
const directParentControl = this.controlContainer?.control;
304+
305+
if (!directParentControl) {
306+
throw new Error(
307+
`[NgxControlError]: A control name cannot be specified without a parent FormGroup.`,
308+
);
309+
}
310+
311+
const controlInstance = directParentControl.get(control);
312+
313+
if (!controlInstance) {
314+
throw new Error(
315+
`[NgxControlError]: Cannot find control with name '${control}'.`,
316+
);
317+
}
318+
319+
this.control$.set(controlInstance);
306320
}
307321

308322
/**
@@ -321,7 +335,7 @@ export class NgxControlError {
321335
}
322336

323337
/**
324-
* The parent of this {@link control$ control}.
338+
* The top level parent of this {@link control$ control}.
325339
*
326340
* NOTE: Might not be the control referenced by {@link AbstractControl.parent parent} of this {@link control$ control}.
327341
*/
@@ -340,7 +354,7 @@ export class NgxControlError {
340354
public readonly track$ = signal<undefined | string | string[]>(undefined);
341355

342356
/**
343-
* The parent of this {@link control$ control}.
357+
* The top level parent of this {@link control$ control}.
344358
*
345359
* NOTE: Might not be the control referenced by {@link AbstractControl.parent parent} of this {@link control$ control}.
346360
*/
@@ -351,6 +365,13 @@ export class NgxControlError {
351365
undefined,
352366
);
353367

368+
/**
369+
* The direct parent form group directive of this control.
370+
*/
371+
private readonly controlContainer = inject(ControlContainer, {
372+
optional: true,
373+
});
374+
354375
/**
355376
* The control which `errors` are tracked.
356377
*

0 commit comments

Comments
 (0)