From 4c93e8c3e4fad08f4563aee607dae93b7c664610 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Thu, 24 Sep 2020 11:36:46 -0400 Subject: [PATCH] feat(cdk/coercion): add coercion for string arrays Co-authored-by: Jan Malchert <25508038+JanMalch@users.noreply.github.com>" --- src/cdk/coercion/public-api.ts | 1 + src/cdk/coercion/string-array.spec.ts | 32 +++++++++++ src/cdk/coercion/string-array.ts | 40 ++++++++++++++ src/material/autocomplete/autocomplete.ts | 8 +-- src/material/datepicker/datepicker-base.ts | 15 ++++-- src/material/datepicker/datepicker.spec.ts | 54 +++++++++++++++++++ tools/public_api_guard/cdk/coercion.d.ts | 2 + .../material/autocomplete.d.ts | 2 +- 8 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 src/cdk/coercion/string-array.spec.ts create mode 100644 src/cdk/coercion/string-array.ts diff --git a/src/cdk/coercion/public-api.ts b/src/cdk/coercion/public-api.ts index a33a133e56bb..69d9eada7376 100644 --- a/src/cdk/coercion/public-api.ts +++ b/src/cdk/coercion/public-api.ts @@ -11,3 +11,4 @@ export * from './number-property'; export * from './array'; export * from './css-pixel-value'; export * from './element'; +export * from './string-array'; diff --git a/src/cdk/coercion/string-array.spec.ts b/src/cdk/coercion/string-array.spec.ts new file mode 100644 index 000000000000..99bb3b3da5c8 --- /dev/null +++ b/src/cdk/coercion/string-array.spec.ts @@ -0,0 +1,32 @@ +import {coerceStringArray} from '@angular/cdk/coercion/string-array'; + +describe('coerceStringArray', () => { + it('should split a string', () => { + expect(coerceStringArray('x y z 1')).toEqual(['x', 'y', 'z', '1']); + }); + + it('should map values to string in an array', () => { + expect(coerceStringArray(['x', 1, true, null, undefined, ['arr', 'ay'], { data: false }])) + .toEqual(['x', '1', 'true', 'null', 'undefined', 'arr,ay', '[object Object]']); + }); + + it('should work with a custom delimiter', () => { + expect(coerceStringArray('1::2::3::4', '::')).toEqual(['1', '2', '3', '4']); + }); + + it('should trim values and remove empty values', () => { + expect(coerceStringArray(', x, ,, ', ',')).toEqual(['x']); + }); + + it('should map non-string values to string', () => { + expect(coerceStringArray(0)).toEqual(['0']); + }); + + it('should return an empty array for null', () => { + expect(coerceStringArray(null)).toEqual([]); + }); + + it('should return an empty array for undefined', () => { + expect(coerceStringArray(undefined)).toEqual([]); + }); +}); diff --git a/src/cdk/coercion/string-array.ts b/src/cdk/coercion/string-array.ts new file mode 100644 index 000000000000..02f5cf5e41a7 --- /dev/null +++ b/src/cdk/coercion/string-array.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * Coerces a value to an array of trimmed non-empty strings. + * Any input that is not an array, `null` or `undefined` will be turned into a string + * via `toString()` and subsequently split with the given separator. + * `null` and `undefined` will result in an empty array. + * This results in the following outcomes: + * - `null` -> `[]` + * - `[null]` -> `["null"]` + * - `["a", "b ", " "]` -> `["a", "b"]` + * - `[1, [2, 3]]` -> `["1", "2,3"]` + * - `[{ a: 0 }]` -> `["[object Object]"]` + * - `{ a: 0 }` -> `["[object", "Object]"]` + * + * Useful for defining CSS classes or table columns. + * @param value the value to coerce into an array of strings + * @param separator split-separator if value isn't an array + */ +export function coerceStringArray(value: any, separator: string | RegExp = /\s+/): string[] { + const result = []; + + if (value != null) { + const sourceValues = Array.isArray(value) ? value : `${value}`.split(separator); + for (const sourceValue of sourceValues) { + const trimmedString = `${sourceValue}`.trim(); + if (trimmedString) { + result.push(trimmedString); + } + } + } + + return result; +} diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts index c7ed69fb80be..7e3f9853566e 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -7,7 +7,7 @@ */ import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion'; import { AfterContentInit, ChangeDetectionStrategy, @@ -169,10 +169,10 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp * inside the overlay container to allow for easy styling. */ @Input('class') - set classList(value: string) { + set classList(value: string | string[]) { if (value && value.length) { - this._classList = value.split(' ').reduce((classList, className) => { - classList[className.trim()] = true; + this._classList = coerceStringArray(value).reduce((classList, className) => { + classList[className] = true; return classList; }, {} as {[key: string]: boolean}); } else { diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index 79d08fe1c6ef..a369ca6a0bce 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -7,7 +7,7 @@ */ import {Directionality} from '@angular/cdk/bidi'; -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion'; import {ESCAPE, UP_ARROW} from '@angular/cdk/keycodes'; import { Overlay, @@ -307,9 +307,6 @@ export abstract class MatDatepickerBase, S, @Output() readonly viewChanged: EventEmitter = new EventEmitter(true); - /** Classes to be passed to the date picker panel. Supports the same syntax as `ngClass`. */ - @Input() panelClass: string | string[]; - /** Function that can be used to add custom CSS classes to dates. */ @Input() dateClass: MatCalendarCellClassFunction; @@ -319,6 +316,16 @@ export abstract class MatDatepickerBase, S, /** Emits when the datepicker has been closed. */ @Output('closed') closedStream: EventEmitter = new EventEmitter(); + /** + * Classes to be passed to the date picker panel. + * Supports string and string array values, similar to `ngClass`. + */ + @Input() + get panelClass(): string | string[] { return this._panelClass; } + set panelClass(value: string | string[]) { + this._panelClass = coerceStringArray(value); + } + private _panelClass: string[]; /** Whether the calendar is open. */ @Input() diff --git a/src/material/datepicker/datepicker.spec.ts b/src/material/datepicker/datepicker.spec.ts index c8c82d5c3f5e..a200d9229ff5 100644 --- a/src/material/datepicker/datepicker.spec.ts +++ b/src/material/datepicker/datepicker.spec.ts @@ -2037,6 +2037,47 @@ describe('MatDatepicker', () => { subscription.unsubscribe(); }); + describe('panelClass input', () => { + let fixture: ComponentFixture; + let testComponent: PanelClassDatepicker; + + beforeEach(fakeAsync(() => { + fixture = createComponent(PanelClassDatepicker, [MatNativeDateModule]); + fixture.detectChanges(); + + testComponent = fixture.componentInstance; + })); + + afterEach(fakeAsync(() => { + testComponent.datepicker.close(); + fixture.detectChanges(); + flush(); + })); + + it('should accept a single class', () => { + testComponent.panelClass = 'foobar'; + fixture.detectChanges(); + expect(testComponent.datepicker.panelClass).toEqual(['foobar']); + }); + + it('should accept multiple classes', () => { + testComponent.panelClass = 'foo bar'; + fixture.detectChanges(); + expect(testComponent.datepicker.panelClass).toEqual(['foo', 'bar']); + }); + + it('should work with ngClass', () => { + testComponent.panelClass = ['foo', 'bar']; + testComponent.datepicker.open(); + fixture.detectChanges(); + + const datepickerContent = testComponent.datepicker['_dialogRef']!!.componentInstance; + const actualClasses = datepickerContent._elementRef.nativeElement.children[1].classList; + expect(actualClasses.contains('foo')).toBe(true); + expect(actualClasses.contains('bar')).toBe(true); + }); + }); + }); /** @@ -2400,3 +2441,16 @@ class DatepickerInputWithCustomValidator { min: Date; max: Date; } + + +@Component({ + template: ` + + + `, +}) +class PanelClassDatepicker { + date = new Date(0); + panelClass: any; + @ViewChild('d') datepicker: MatDatepicker; +} diff --git a/tools/public_api_guard/cdk/coercion.d.ts b/tools/public_api_guard/cdk/coercion.d.ts index 72cc86a324bb..67d4eb495ca2 100644 --- a/tools/public_api_guard/cdk/coercion.d.ts +++ b/tools/public_api_guard/cdk/coercion.d.ts @@ -14,4 +14,6 @@ export declare function coerceElement(elementOrRef: ElementRef | T): T; export declare function coerceNumberProperty(value: any): number; export declare function coerceNumberProperty(value: any, fallback: D): number | D; +export declare function coerceStringArray(value: any, separator?: string | RegExp): string[]; + export declare type NumberInput = string | number | null | undefined; diff --git a/tools/public_api_guard/material/autocomplete.d.ts b/tools/public_api_guard/material/autocomplete.d.ts index 8fc572211c7f..aa85d12288fa 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -8,7 +8,7 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin protected abstract _visibleClass: string; get autoActiveFirstOption(): boolean; set autoActiveFirstOption(value: boolean); - set classList(value: string); + set classList(value: string | string[]); readonly closed: EventEmitter; displayWith: ((value: any) => string) | null; id: string;