Skip to content

Commit a14eeb4

Browse files
authored
feat(cdk/coercion): add coercion for string arrays (#20652)
Co-authored-by: Jan Malchert <25508038+JanMalch@users.noreply.github.com>"
1 parent 40fa9ff commit a14eeb4

File tree

8 files changed

+145
-9
lines changed

8 files changed

+145
-9
lines changed

src/cdk/coercion/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './number-property';
1111
export * from './array';
1212
export * from './css-pixel-value';
1313
export * from './element';
14+
export * from './string-array';

src/cdk/coercion/string-array.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {coerceStringArray} from '@angular/cdk/coercion/string-array';
2+
3+
describe('coerceStringArray', () => {
4+
it('should split a string', () => {
5+
expect(coerceStringArray('x y z 1')).toEqual(['x', 'y', 'z', '1']);
6+
});
7+
8+
it('should map values to string in an array', () => {
9+
expect(coerceStringArray(['x', 1, true, null, undefined, ['arr', 'ay'], { data: false }]))
10+
.toEqual(['x', '1', 'true', 'null', 'undefined', 'arr,ay', '[object Object]']);
11+
});
12+
13+
it('should work with a custom delimiter', () => {
14+
expect(coerceStringArray('1::2::3::4', '::')).toEqual(['1', '2', '3', '4']);
15+
});
16+
17+
it('should trim values and remove empty values', () => {
18+
expect(coerceStringArray(', x, ,, ', ',')).toEqual(['x']);
19+
});
20+
21+
it('should map non-string values to string', () => {
22+
expect(coerceStringArray(0)).toEqual(['0']);
23+
});
24+
25+
it('should return an empty array for null', () => {
26+
expect(coerceStringArray(null)).toEqual([]);
27+
});
28+
29+
it('should return an empty array for undefined', () => {
30+
expect(coerceStringArray(undefined)).toEqual([]);
31+
});
32+
});

src/cdk/coercion/string-array.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* Coerces a value to an array of trimmed non-empty strings.
11+
* Any input that is not an array, `null` or `undefined` will be turned into a string
12+
* via `toString()` and subsequently split with the given separator.
13+
* `null` and `undefined` will result in an empty array.
14+
* This results in the following outcomes:
15+
* - `null` -&gt; `[]`
16+
* - `[null]` -&gt; `["null"]`
17+
* - `["a", "b ", " "]` -&gt; `["a", "b"]`
18+
* - `[1, [2, 3]]` -&gt; `["1", "2,3"]`
19+
* - `[{ a: 0 }]` -&gt; `["[object Object]"]`
20+
* - `{ a: 0 }` -&gt; `["[object", "Object]"]`
21+
*
22+
* Useful for defining CSS classes or table columns.
23+
* @param value the value to coerce into an array of strings
24+
* @param separator split-separator if value isn't an array
25+
*/
26+
export function coerceStringArray(value: any, separator: string | RegExp = /\s+/): string[] {
27+
const result = [];
28+
29+
if (value != null) {
30+
const sourceValues = Array.isArray(value) ? value : `${value}`.split(separator);
31+
for (const sourceValue of sourceValues) {
32+
const trimmedString = `${sourceValue}`.trim();
33+
if (trimmedString) {
34+
result.push(trimmedString);
35+
}
36+
}
37+
}
38+
39+
return result;
40+
}

src/material/autocomplete/autocomplete.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
10-
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10+
import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion';
1111
import {
1212
AfterContentInit,
1313
ChangeDetectionStrategy,
@@ -169,10 +169,10 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp
169169
* inside the overlay container to allow for easy styling.
170170
*/
171171
@Input('class')
172-
set classList(value: string) {
172+
set classList(value: string | string[]) {
173173
if (value && value.length) {
174-
this._classList = value.split(' ').reduce((classList, className) => {
175-
classList[className.trim()] = true;
174+
this._classList = coerceStringArray(value).reduce((classList, className) => {
175+
classList[className] = true;
176176
return classList;
177177
}, {} as {[key: string]: boolean});
178178
} else {

src/material/datepicker/datepicker-base.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {Directionality} from '@angular/cdk/bidi';
10-
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10+
import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion';
1111
import {ESCAPE, UP_ARROW} from '@angular/cdk/keycodes';
1212
import {
1313
Overlay,
@@ -307,9 +307,6 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
307307
@Output() readonly viewChanged: EventEmitter<MatCalendarView> =
308308
new EventEmitter<MatCalendarView>(true);
309309

310-
/** Classes to be passed to the date picker panel. Supports the same syntax as `ngClass`. */
311-
@Input() panelClass: string | string[];
312-
313310
/** Function that can be used to add custom CSS classes to dates. */
314311
@Input() dateClass: MatCalendarCellClassFunction<D>;
315312

@@ -319,6 +316,16 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
319316
/** Emits when the datepicker has been closed. */
320317
@Output('closed') closedStream: EventEmitter<void> = new EventEmitter<void>();
321318

319+
/**
320+
* Classes to be passed to the date picker panel.
321+
* Supports string and string array values, similar to `ngClass`.
322+
*/
323+
@Input()
324+
get panelClass(): string | string[] { return this._panelClass; }
325+
set panelClass(value: string | string[]) {
326+
this._panelClass = coerceStringArray(value);
327+
}
328+
private _panelClass: string[];
322329

323330
/** Whether the calendar is open. */
324331
@Input()

src/material/datepicker/datepicker.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,47 @@ describe('MatDatepicker', () => {
20372037
subscription.unsubscribe();
20382038
});
20392039

2040+
describe('panelClass input', () => {
2041+
let fixture: ComponentFixture<PanelClassDatepicker>;
2042+
let testComponent: PanelClassDatepicker;
2043+
2044+
beforeEach(fakeAsync(() => {
2045+
fixture = createComponent(PanelClassDatepicker, [MatNativeDateModule]);
2046+
fixture.detectChanges();
2047+
2048+
testComponent = fixture.componentInstance;
2049+
}));
2050+
2051+
afterEach(fakeAsync(() => {
2052+
testComponent.datepicker.close();
2053+
fixture.detectChanges();
2054+
flush();
2055+
}));
2056+
2057+
it('should accept a single class', () => {
2058+
testComponent.panelClass = 'foobar';
2059+
fixture.detectChanges();
2060+
expect(testComponent.datepicker.panelClass).toEqual(['foobar']);
2061+
});
2062+
2063+
it('should accept multiple classes', () => {
2064+
testComponent.panelClass = 'foo bar';
2065+
fixture.detectChanges();
2066+
expect(testComponent.datepicker.panelClass).toEqual(['foo', 'bar']);
2067+
});
2068+
2069+
it('should work with ngClass', () => {
2070+
testComponent.panelClass = ['foo', 'bar'];
2071+
testComponent.datepicker.open();
2072+
fixture.detectChanges();
2073+
2074+
const datepickerContent = testComponent.datepicker['_dialogRef']!!.componentInstance;
2075+
const actualClasses = datepickerContent._elementRef.nativeElement.children[1].classList;
2076+
expect(actualClasses.contains('foo')).toBe(true);
2077+
expect(actualClasses.contains('bar')).toBe(true);
2078+
});
2079+
});
2080+
20402081
});
20412082

20422083
/**
@@ -2400,3 +2441,16 @@ class DatepickerInputWithCustomValidator {
24002441
min: Date;
24012442
max: Date;
24022443
}
2444+
2445+
2446+
@Component({
2447+
template: `
2448+
<input [matDatepicker]="d" [value]="date">
2449+
<mat-datepicker [panelClass]="panelClass" touchUi #d></mat-datepicker>
2450+
`,
2451+
})
2452+
class PanelClassDatepicker {
2453+
date = new Date(0);
2454+
panelClass: any;
2455+
@ViewChild('d') datepicker: MatDatepicker<Date>;
2456+
}

tools/public_api_guard/cdk/coercion.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ export declare function coerceElement<T>(elementOrRef: ElementRef<T> | T): T;
1414
export declare function coerceNumberProperty(value: any): number;
1515
export declare function coerceNumberProperty<D>(value: any, fallback: D): number | D;
1616

17+
export declare function coerceStringArray(value: any, separator?: string | RegExp): string[];
18+
1719
export declare type NumberInput = string | number | null | undefined;

tools/public_api_guard/material/autocomplete.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin
88
protected abstract _visibleClass: string;
99
get autoActiveFirstOption(): boolean;
1010
set autoActiveFirstOption(value: boolean);
11-
set classList(value: string);
11+
set classList(value: string | string[]);
1212
readonly closed: EventEmitter<void>;
1313
displayWith: ((value: any) => string) | null;
1414
id: string;

0 commit comments

Comments
 (0)