New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): introduce KeyValuePipe #24319

Closed
wants to merge 6 commits into
base: master
from

feat(common): introduce KeyValuePipe

  • Loading branch information...
Toxicable committed Jun 6, 2018
commit 5cde92a05e65340a886b53ff3019fe04710be8d1
Copy path View file
@@ -21,7 +21,7 @@ export {parseCookieValue as ɵparseCookieValue} from './cookie';
export {CommonModule, DeprecatedI18NPipesModule} from './common_module';
export {NgClass, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
export {DOCUMENT} from './dom_tokens';
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index';
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe, KeyValuePipe, KeyValue} from './pipes/index';
export {DeprecatedDatePipe, DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './pipes/deprecated/index';
export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id';
export {VERSION} from './version';
@@ -17,6 +17,7 @@ import {DatePipe} from './date_pipe';
import {I18nPluralPipe} from './i18n_plural_pipe';
import {I18nSelectPipe} from './i18n_select_pipe';
import {JsonPipe} from './json_pipe';
import {KeyValue, KeyValuePipe} from './keyvalue_pipe';
import {CurrencyPipe, DecimalPipe, PercentPipe} from './number_pipe';
import {SlicePipe} from './slice_pipe';

@@ -25,14 +26,16 @@ export {
CurrencyPipe,
DatePipe,
DecimalPipe,
KeyValue,
KeyValuePipe,
I18nPluralPipe,
I18nSelectPipe,
JsonPipe,
LowerCasePipe,
PercentPipe,
SlicePipe,
TitleCasePipe,
UpperCasePipe
UpperCasePipe,
};


@@ -52,4 +55,5 @@ export const COMMON_PIPES = [
DatePipe,
I18nPluralPipe,
I18nSelectPipe,
KeyValuePipe,
];
@@ -0,0 +1,110 @@
/**
* @license
* Copyright Google Inc. 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
*/

import {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Pipe, PipeTransform} from '@angular/core';

function makeKeyValuePair<K, V>(key: K, value: V): KeyValue<K, V> {
return {key: key, value: value};
}

/**
* A key value pair.
* Usually used to represent the key value pairs from a Map or Object.
*/
export interface KeyValue<K, V> {
key: K;
value: V;
}

/**
* @ngModule CommonModule
* @description
*
* Transforms Object or Map into an array of key value pairs.
*
* The output array will be ordered by keys.
* By default the comparator will be by Unicode point value.
* You can optionally pass a compareFn if your keys are complex types.
*

This comment has been minimized.

@mhevery

mhevery Jun 11, 2018

Member

Could you add an example of usage. We are trying to make sure that all new APIs have an example. See as an example of how to do it https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L43-L68. Notice that the example https://github.com/angular/angular/blob/master/packages/examples/common/pipes/ts/async_pipe.ts has tests https://github.com/angular/angular/blob/master/packages/examples/common/pipes/ts/e2e_test/pipe_spec.ts which verifies that the example does not get broken.

* ## Examples
*
* This examples show how an Object or a Map and be iterated by ngFor with the use of this keyvalue
* pipe.
*
* {@example common/pipes/ts/keyvalue.ts region='KeyValuePipe'}
*/
@Pipe({name: 'keyvalue', pure: false})
export class KeyValuePipe implements PipeTransform {

This comment has been minimized.

@alfaproject

alfaproject Jun 13, 2018

Contributor

Any reason not to do KeyValuePipe<K, V>? It down-levels to the exact same code and would avoid having to use any, I think.

constructor(private readonly differs: KeyValueDiffers) {}

private differ: KeyValueDiffer<any, any>;
private keyValues: Array<KeyValue<any, any>>;

transform<K, V>(input: null, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): null;
transform<V>(
input: {[key: string]: V}|Map<string, V>,
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
Array<KeyValue<string, V>>;
transform<V>(
input: {[key: number]: V}|Map<number, V>,
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
Array<KeyValue<number, V>>;
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number):
Array<KeyValue<K, V>>;
transform<K, V>(
input: null|{[key: string]: V, [key: number]: V}|Map<K, V>,
compareFn: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number = defaultComparator):
Array<KeyValue<K, V>>|null {
if (!input || (!(input instanceof Map) && typeof input !== 'object')) {
return null;
}

if (!this.differ) {
// make a differ for whatever type we've been passed in
this.differ = this.differs.find(input).create();
}

const differChanges: KeyValueChanges<K, V>|null = this.differ.diff(input as any);

if (differChanges) {
this.keyValues = [];
differChanges.forEachItem((r: KeyValueChangeRecord<K, V>) => {
this.keyValues.push(makeKeyValuePair(r.key, r.currentValue !));
});
this.keyValues.sort(compareFn);
}
return this.keyValues;
}
}

export function defaultComparator<K, V>(
keyValueA: KeyValue<K, V>, keyValueB: KeyValue<K, V>): number {
const a = keyValueA.key;
const b = keyValueB.key;
// if same exit with 0;
if (a === b) return 0;
// make sure that undefined are at the end of the sort.
if (a === undefined) return 1;
if (b === undefined) return -1;
// make sure that nulls are at the end of the sort.
if (a === null) return 1;
if (b === null) return -1;
if (typeof a == 'string' && typeof b == 'string') {
return a < b ? -1 : 1;
}
if (typeof a == 'number' && typeof b == 'number') {
return a - b;
}
if (typeof a == 'boolean' && typeof b == 'boolean') {
return a < b ? -1 : 1;
}
// `a` and `b` are of different types. Compare their string values.
const aString = String(a);
const bString = String(b);
return aString == bString ? 0 : aString < bString ? -1 : 1;
}
@@ -0,0 +1,152 @@
/**
* @license
* Copyright Google Inc. 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
*/

import {KeyValuePipe} from '@angular/common';
import {EventEmitter, KeyValueDiffers, WrappedValue, ɵdefaultKeyValueDiffers as defaultKeyValueDiffers} from '@angular/core';
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';

import {defaultComparator} from '../../src/pipes/keyvalue_pipe';
import {SpyChangeDetectorRef} from '../spies';

describe('KeyValuePipe', () => {
it('should return null when given null', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform(null)).toEqual(null);
});
it('should return null when given undefined', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform(undefined as any)).toEqual(null);
});
it('should return null for an unsupported type', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const fn = () => {};
expect(pipe.transform(fn as any)).toEqual(null);
});
describe('object dictionary', () => {
it('should transform a basic dictionary', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform({1: 2})).toEqual([{key: '1', value: 2}]);
});
it('should order by alpha', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform({'b': 1, 'a': 1})).toEqual([
{key: 'a', value: 1}, {key: 'b', value: 1}
]);
});
it('should order by numerical', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform({2: 1, 1: 1})).toEqual([{key: '1', value: 1}, {key: '2', value: 1}]);
});
it('should order by numerical and alpha', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const input = {2: 1, 1: 1, 'b': 1, 0: 1, 3: 1, 'a': 1};
expect(pipe.transform(input)).toEqual([
{key: '0', value: 1}, {key: '1', value: 1}, {key: '2', value: 1}, {key: '3', value: 1},
{key: 'a', value: 1}, {key: 'b', value: 1}
]);
});
it('should return the same ref if nothing changes', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const transform1 = pipe.transform({1: 2});
const transform2 = pipe.transform({1: 2});
expect(transform1 === transform2).toEqual(true);
});
it('should return a new ref if something changes', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const transform1 = pipe.transform({1: 2});
const transform2 = pipe.transform({1: 3});
expect(transform1 !== transform2).toEqual(true);
});
});

describe('Map', () => {
it('should transform a basic Map', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform(new Map([[1, 2]]))).toEqual([{key: 1, value: 2}]);
});
it('should order by alpha', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform(new Map([['b', 1], ['a', 1]]))).toEqual([
{key: 'a', value: 1}, {key: 'b', value: 1}
]);
});
it('should order by numerical', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform(new Map([[2, 1], [1, 1]]))).toEqual([
{key: 1, value: 1}, {key: 2, value: 1}
]);
});
it('should order by numerical and alpha', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const input = [[2, 1], [1, 1], ['b', 1], [0, 1], [3, 1], ['a', 1]];
expect(pipe.transform(new Map(input as any))).toEqual([
{key: 0, value: 1}, {key: 1, value: 1}, {key: 2, value: 1}, {key: 3, value: 1},
{key: 'a', value: 1}, {key: 'b', value: 1}
]);
});
it('should order by complex types with compareFn', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const input = new Map([[{id: 1}, 1], [{id: 0}, 1]]);
expect(pipe.transform<{id: number}, number>(input, (a, b) => a.key.id > b.key.id ? 1 : 2))
.toEqual([
{key: {id: 0}, value: 1},
{key: {id: 1}, value: 1},
]);
});
it('should return the same ref if nothing changes', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const transform1 = pipe.transform(new Map([[1, 2]]));
const transform2 = pipe.transform(new Map([[1, 2]]));
expect(transform1 === transform2).toEqual(true);
});
it('should return a new ref if something changes', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const transform1 = pipe.transform(new Map([[1, 2]]));
const transform2 = pipe.transform(new Map([[1, 3]]));
expect(transform1 !== transform2).toEqual(true);
});
});
});

describe('defaultComparator', () => {
it('should remain the same order when keys are equal', () => {
const key = 1;
const values = [{key, value: 2}, {key, value: 1}];
expect(values.sort(defaultComparator)).toEqual(values);
});
it('should sort undefined keys to the end', () => {
const values = [{key: 3, value: 1}, {key: undefined, value: 3}, {key: 1, value: 2}];
expect(values.sort(defaultComparator)).toEqual([
{key: 1, value: 2}, {key: 3, value: 1}, {key: undefined, value: 3}
]);
});
it('should sort null keys to the end', () => {
const values = [{key: 3, value: 1}, {key: null, value: 3}, {key: 1, value: 2}];
expect(values.sort(defaultComparator)).toEqual([
{key: 1, value: 2}, {key: 3, value: 1}, {key: null, value: 3}
]);
});
it('should sort strings in alpha ascending', () => {
const values = [{key: 'b', value: 1}, {key: 'a', value: 3}];
expect(values.sort(defaultComparator)).toEqual([{key: 'a', value: 3}, {key: 'b', value: 1}]);
});
it('should sort numbers in numerical ascending', () => {
const values = [{key: 2, value: 1}, {key: 1, value: 3}];
expect(values.sort(defaultComparator)).toEqual([{key: 1, value: 3}, {key: 2, value: 1}]);
});
it('should sort boolean in false (0) -> true (1)', () => {
const values = [{key: true, value: 3}, {key: false, value: 1}];
expect(values.sort(defaultComparator)).toEqual([{key: false, value: 1}, {key: true, value: 3}]);
});
it('should sort numbers as strings in numerical ascending', () => {
const values = [{key: '2', value: 1}, {key: 1, value: 3}];
expect(values.sort(defaultComparator)).toEqual([{key: 1, value: 3}, {key: '2', value: 1}]);
});
});
@@ -22,7 +22,7 @@ export interface KeyValueDiffer<K, V> {
* @returns an object describing the difference. The return value is only valid until the next
* `diff()` invocation.
*/
diff(object: Map<K, V>): KeyValueChanges<K, V>;
diff(object: Map<K, V>): KeyValueChanges<K, V>|null;

/**
* Compute a difference between the previous state and the new `object` state.
@@ -31,7 +31,7 @@ export interface KeyValueDiffer<K, V> {
* @returns an object describing the difference. The return value is only valid until the next
* `diff()` invocation.
*/
diff(object: {[key: string]: V}): KeyValueChanges<string, V>;
diff(object: {[key: string]: V}): KeyValueChanges<string, V>|null;
// TODO(TS2.1): diff<KP extends string>(this: KeyValueDiffer<KP, V>, object: Record<KP, V>):
// KeyValueDiffer<KP, V>;
}
@@ -57,4 +57,14 @@ describe('pipe', () => {
expect(element.all(by.css('titlecase-pipe p')).get(5).getText()).toEqual('Foo-vs-bar');
});
});
describe('titlecase', () => {
it('should work properly', () => {
browser.get(URL);
waitForElement('keyvalue-pipe');
expect(element.all(by.css('keyvalue-pipe div')).get(0).getText()).toEqual('1:foo');
expect(element.all(by.css('keyvalue-pipe div')).get(1).getText()).toEqual('2:bar');
expect(element.all(by.css('keyvalue-pipe div')).get(2).getText()).toEqual('1:foo');
expect(element.all(by.css('keyvalue-pipe div')).get(3).getText()).toEqual('2:bar');
});
});
});
@@ -0,0 +1,29 @@
/**
* @license
* Copyright Google Inc. 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
*/

import {Component} from '@angular/core';

// #docregion KeyValuePipe
@Component({
selector: 'keyvalue-pipe',
template: `<span>
<p>Object</p>
<div *ngFor="let item of object | keyvalue">
{{item.key}}:{{item.value}}
</div>
<p>Map</p>
<div *ngFor="let item of map | keyvalue">
{{item.key}}:{{item.value}}
</div>
</span>`
})
export class KeyValuePipeComponent {
object: {[key: number]: string} = {2: 'foo', 1: 'bar'};
map = new Map([[2, 'foo'], [1, 'bar']]);
}
// #enddocregion
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.