Skip to content

Commit

Permalink
feat(cdk/coercion): add coercion for string arrays (#20652)
Browse files Browse the repository at this point in the history
Co-authored-by: Jan Malchert <25508038+JanMalch@users.noreply.github.com>"
  • Loading branch information
annieyw committed Oct 2, 2020
1 parent 40fa9ff commit a14eeb4
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/cdk/coercion/public-api.ts
Expand Up @@ -11,3 +11,4 @@ export * from './number-property';
export * from './array';
export * from './css-pixel-value';
export * from './element';
export * from './string-array';
32 changes: 32 additions & 0 deletions 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([]);
});
});
40 changes: 40 additions & 0 deletions 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` -&gt; `[]`
* - `[null]` -&gt; `["null"]`
* - `["a", "b ", " "]` -&gt; `["a", "b"]`
* - `[1, [2, 3]]` -&gt; `["1", "2,3"]`
* - `[{ a: 0 }]` -&gt; `["[object Object]"]`
* - `{ a: 0 }` -&gt; `["[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;
}
8 changes: 4 additions & 4 deletions src/material/autocomplete/autocomplete.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 11 additions & 4 deletions src/material/datepicker/datepicker-base.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -307,9 +307,6 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
@Output() readonly viewChanged: EventEmitter<MatCalendarView> =
new EventEmitter<MatCalendarView>(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<D>;

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

/**
* 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()
Expand Down
54 changes: 54 additions & 0 deletions src/material/datepicker/datepicker.spec.ts
Expand Up @@ -2037,6 +2037,47 @@ describe('MatDatepicker', () => {
subscription.unsubscribe();
});

describe('panelClass input', () => {
let fixture: ComponentFixture<PanelClassDatepicker>;
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);
});
});

});

/**
Expand Down Expand Up @@ -2400,3 +2441,16 @@ class DatepickerInputWithCustomValidator {
min: Date;
max: Date;
}


@Component({
template: `
<input [matDatepicker]="d" [value]="date">
<mat-datepicker [panelClass]="panelClass" touchUi #d></mat-datepicker>
`,
})
class PanelClassDatepicker {
date = new Date(0);
panelClass: any;
@ViewChild('d') datepicker: MatDatepicker<Date>;
}
2 changes: 2 additions & 0 deletions tools/public_api_guard/cdk/coercion.d.ts
Expand Up @@ -14,4 +14,6 @@ export declare function coerceElement<T>(elementOrRef: ElementRef<T> | T): T;
export declare function coerceNumberProperty(value: any): number;
export declare function coerceNumberProperty<D>(value: any, fallback: D): number | D;

export declare function coerceStringArray(value: any, separator?: string | RegExp): string[];

export declare type NumberInput = string | number | null | undefined;
2 changes: 1 addition & 1 deletion tools/public_api_guard/material/autocomplete.d.ts
Expand Up @@ -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<void>;
displayWith: ((value: any) => string) | null;
id: string;
Expand Down

0 comments on commit a14eeb4

Please sign in to comment.