Skip to content

Commit 348f149

Browse files
mmalerbaAndrewKushnir
authored andcommitted
feat(forms): pass field directive to class config
Updates signal forms to pass the full `Field` directive to the class configuration functions, rather than just the state. This allows developers to take the element as well as the state into consideration when deciding classes to apply. Closes angular#65762 BREAKING CHANGE: The shape of `SignalFormsConfig.classes` has changed Previously each function in the `classes` map took a `FieldState`. Now it takes a `Field` directive. For example if you previously had: ``` provideSignalFormsConfig({ classes: { 'my-valid': (state) => state.valid() } }) ``` You would need to update to: ``` provideSignalFormsConfig({ classes: { 'my-valid': ({state}) => state().valid() } }) ```
1 parent 7be4dde commit 348f149

5 files changed

Lines changed: 42 additions & 12 deletions

File tree

goldens/public-api/forms/signals/index.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
520520
// @public
521521
export interface SignalFormsConfig {
522522
classes?: {
523-
[className: string]: (state: FieldState<unknown>) => boolean;
523+
[className: string]: (state: Field<unknown>) => boolean;
524524
};
525525
}
526526

packages/forms/signals/compat/src/api/di.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import type {SignalFormsConfig} from '../../../src/api/di';
1515
* @experimental 21.0.1
1616
*/
1717
export const NG_STATUS_CLASSES: SignalFormsConfig['classes'] = {
18-
'ng-touched': (state) => state.touched(),
19-
'ng-untouched': (state) => !state.touched(),
20-
'ng-dirty': (state) => state.dirty(),
21-
'ng-pristine': (state) => !state.dirty(),
22-
'ng-valid': (state) => state.valid(),
23-
'ng-invalid': (state) => state.invalid(),
24-
'ng-pending': (state) => state.pending(),
18+
'ng-touched': ({state}) => state().touched(),
19+
'ng-untouched': ({state}) => !state().touched(),
20+
'ng-dirty': ({state}) => state().dirty(),
21+
'ng-pristine': ({state}) => !state().dirty(),
22+
'ng-valid': ({state}) => state().valid(),
23+
'ng-invalid': ({state}) => state().invalid(),
24+
'ng-pending': ({state}) => state().pending(),
2525
};

packages/forms/signals/src/api/di.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {type Provider} from '@angular/core';
1010
import {SIGNAL_FORMS_CONFIG} from '../field/di';
11-
import type {FieldState} from './types';
11+
import type {Field} from './field_directive';
1212

1313
/**
1414
* Configuration options for signal forms.
@@ -17,7 +17,7 @@ import type {FieldState} from './types';
1717
*/
1818
export interface SignalFormsConfig {
1919
/** A map of CSS class names to predicate functions that determine when to apply them. */
20-
classes?: {[className: string]: (state: FieldState<unknown>) => boolean};
20+
classes?: {[className: string]: (state: Field<unknown>) => boolean};
2121
}
2222

2323
/**

packages/forms/signals/src/api/field_directive.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ export class Field<T> {
8585
private config = inject(SIGNAL_FORMS_CONFIG, {optional: true});
8686
/** @internal */
8787
readonly classes = Object.entries(this.config?.classes ?? {}).map(
88-
([className, computation]) => [className, computed(() => computation(this.state()))] as const,
88+
([className, computation]) =>
89+
[className, computed(() => computation(this as Field<unknown>))] as const,
8990
);
9091

9192
/** Any `ControlValueAccessor` instances provided on the host element. */

packages/forms/signals/test/web/field_directive.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2653,7 +2653,7 @@ describe('field directive', () => {
26532653
providers: [
26542654
provideSignalFormsConfig({
26552655
classes: {
2656-
'my-invalid-class': (state) => state.invalid(),
2656+
'my-invalid-class': ({state}) => state().invalid(),
26572657
},
26582658
}),
26592659
],
@@ -2770,6 +2770,35 @@ describe('field directive', () => {
27702770
expect(customCtrl.classList.contains('always')).toBe(true);
27712771
expect(customSubform.classList.contains('always')).toBe(false);
27722772
});
2773+
2774+
it('should apply classes based on element', () => {
2775+
TestBed.configureTestingModule({
2776+
providers: [
2777+
provideSignalFormsConfig({
2778+
classes: {
2779+
'multiline': ({element}) => element.tagName.toLowerCase() === 'textarea',
2780+
},
2781+
}),
2782+
],
2783+
});
2784+
2785+
@Component({
2786+
imports: [Field],
2787+
template: `
2788+
<input [field]="f">
2789+
<textarea [field]="f"></textarea>
2790+
`,
2791+
})
2792+
class TestCmp {
2793+
readonly f = form(signal(''));
2794+
}
2795+
2796+
const fixture = act(() => TestBed.createComponent(TestCmp));
2797+
const input = fixture.nativeElement.querySelector('input');
2798+
const textarea = fixture.nativeElement.querySelector('textarea');
2799+
expect(input.classList.contains('multiline')).toBe(false);
2800+
expect(textarea.classList.contains('multiline')).toBe(true);
2801+
});
27732802
});
27742803

27752804
it('should create & bind input when a macro task is running', async () => {

0 commit comments

Comments
 (0)