mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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 #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()
}
})
```
(cherry picked from commit 348f149e8b)
This commit is contained in:
parent
4c8fb3631d
commit
81772b420d
5 changed files with 42 additions and 12 deletions
|
|
@ -520,7 +520,7 @@ export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
|
|||
// @public
|
||||
export interface SignalFormsConfig {
|
||||
classes?: {
|
||||
[className: string]: (state: FieldState<unknown>) => boolean;
|
||||
[className: string]: (state: Field<unknown>) => boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ import type {SignalFormsConfig} from '../../../src/api/di';
|
|||
* @experimental 21.0.1
|
||||
*/
|
||||
export const NG_STATUS_CLASSES: SignalFormsConfig['classes'] = {
|
||||
'ng-touched': (state) => state.touched(),
|
||||
'ng-untouched': (state) => !state.touched(),
|
||||
'ng-dirty': (state) => state.dirty(),
|
||||
'ng-pristine': (state) => !state.dirty(),
|
||||
'ng-valid': (state) => state.valid(),
|
||||
'ng-invalid': (state) => state.invalid(),
|
||||
'ng-pending': (state) => state.pending(),
|
||||
'ng-touched': ({state}) => state().touched(),
|
||||
'ng-untouched': ({state}) => !state().touched(),
|
||||
'ng-dirty': ({state}) => state().dirty(),
|
||||
'ng-pristine': ({state}) => !state().dirty(),
|
||||
'ng-valid': ({state}) => state().valid(),
|
||||
'ng-invalid': ({state}) => state().invalid(),
|
||||
'ng-pending': ({state}) => state().pending(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {type Provider} from '@angular/core';
|
||||
import {SIGNAL_FORMS_CONFIG} from '../field/di';
|
||||
import type {FieldState} from './types';
|
||||
import type {Field} from './field_directive';
|
||||
|
||||
/**
|
||||
* Configuration options for signal forms.
|
||||
|
|
@ -17,7 +17,7 @@ import type {FieldState} from './types';
|
|||
*/
|
||||
export interface SignalFormsConfig {
|
||||
/** A map of CSS class names to predicate functions that determine when to apply them. */
|
||||
classes?: {[className: string]: (state: FieldState<unknown>) => boolean};
|
||||
classes?: {[className: string]: (state: Field<unknown>) => boolean};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ export class Field<T> {
|
|||
private config = inject(SIGNAL_FORMS_CONFIG, {optional: true});
|
||||
/** @internal */
|
||||
readonly classes = Object.entries(this.config?.classes ?? {}).map(
|
||||
([className, computation]) => [className, computed(() => computation(this.state()))] as const,
|
||||
([className, computation]) =>
|
||||
[className, computed(() => computation(this as Field<unknown>))] as const,
|
||||
);
|
||||
|
||||
/** Any `ControlValueAccessor` instances provided on the host element. */
|
||||
|
|
|
|||
|
|
@ -2653,7 +2653,7 @@ describe('field directive', () => {
|
|||
providers: [
|
||||
provideSignalFormsConfig({
|
||||
classes: {
|
||||
'my-invalid-class': (state) => state.invalid(),
|
||||
'my-invalid-class': ({state}) => state().invalid(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
@ -2770,6 +2770,35 @@ describe('field directive', () => {
|
|||
expect(customCtrl.classList.contains('always')).toBe(true);
|
||||
expect(customSubform.classList.contains('always')).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply classes based on element', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideSignalFormsConfig({
|
||||
classes: {
|
||||
'multiline': ({element}) => element.tagName.toLowerCase() === 'textarea',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@Component({
|
||||
imports: [Field],
|
||||
template: `
|
||||
<input [field]="f">
|
||||
<textarea [field]="f"></textarea>
|
||||
`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fixture.nativeElement.querySelector('input');
|
||||
const textarea = fixture.nativeElement.querySelector('textarea');
|
||||
expect(input.classList.contains('multiline')).toBe(false);
|
||||
expect(textarea.classList.contains('multiline')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create & bind input when a macro task is running', async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue