-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Expand file tree
/
Copy pathkeyvalue_pipe.ts
More file actions
169 lines (155 loc) Β· 5.44 KB
/
keyvalue_pipe.ts
File metadata and controls
169 lines (155 loc) Β· 5.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
* @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.dev/license
*/
import {
KeyValueChangeRecord,
KeyValueChanges,
KeyValueDiffer,
KeyValueDiffers,
Pipe,
PipeTransform,
} from '@angular/core';
import {warnIfSignal} from './utils';
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.
*
* @publicApi
*/
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.
* Passing `null` as the compareFn will use natural ordering of the input.
*
* @usageNotes
* ### Examples
*
* This examples show how an Object or a Map can be iterated by ngFor with the use of this
* keyvalue pipe.
*
* {@example common/pipes/ts/keyvalue_pipe.ts region='KeyValuePipe'}
*
* @see [Built-in Pipes](guide/templates/pipes#built-in-pipes)
*
* @publicApi
*/
@Pipe({
name: 'keyvalue',
pure: false,
})
export class KeyValuePipe implements PipeTransform {
constructor(private readonly differs: KeyValueDiffers) {}
private differ!: KeyValueDiffer<any, any>;
private keyValues: Array<KeyValue<any, any>> = [];
private compareFn: ((a: KeyValue<any, any>, b: KeyValue<any, any>) => number) | null =
defaultComparator;
/*
* NOTE: when the `input` value is a simple Record<K, V> object, the keys are extracted with
* Object.keys(). This means that even if the `input` type is Record<number, V> the keys are
* compared/returned as `string`s.
*/
transform<K, V>(
input: ReadonlyMap<K, V>,
compareFn?: ((a: KeyValue<K, V>, b: KeyValue<K, V>) => number) | null,
): Array<KeyValue<K, V>>;
transform<K extends number, V>(
input: Record<K, V>,
compareFn?: ((a: KeyValue<string, V>, b: KeyValue<string, V>) => number) | null,
): Array<KeyValue<string, V>>;
transform<K extends string, V>(
input: Record<K, V> | ReadonlyMap<K, V>,
compareFn?: ((a: KeyValue<K, V>, b: KeyValue<K, V>) => number) | null,
): Array<KeyValue<K, V>>;
transform(
input: null | undefined,
compareFn?: ((a: KeyValue<unknown, unknown>, b: KeyValue<unknown, unknown>) => number) | null,
): null;
transform<K, V>(
input: ReadonlyMap<K, V> | null | undefined,
compareFn?: ((a: KeyValue<K, V>, b: KeyValue<K, V>) => number) | null,
): Array<KeyValue<K, V>> | null;
transform<K extends number, V>(
input: Record<K, V> | null | undefined,
compareFn?: ((a: KeyValue<string, V>, b: KeyValue<string, V>) => number) | null,
): Array<KeyValue<string, V>> | null;
transform<K extends string, V>(
input: Record<K, V> | ReadonlyMap<K, V> | null | undefined,
compareFn?: ((a: KeyValue<K, V>, b: KeyValue<K, V>) => number) | null,
): Array<KeyValue<K, V>> | null;
transform<T>(
input: T,
compareFn?: T extends object ? (a: T[keyof T], b: T[keyof T]) => number : never,
): T extends object ? Array<KeyValue<keyof T, T[keyof T]>> : null;
transform<K, V>(
input: undefined | null | {[key: string]: V; [key: number]: V} | ReadonlyMap<K, V>,
compareFn: ((a: KeyValue<K, V>, b: KeyValue<K, V>) => number) | null = defaultComparator,
): Array<KeyValue<K, V>> | null {
ngDevMode && warnIfSignal('KeyValuePipe', input);
if (!input || (!(input instanceof Map) && typeof input !== 'object')) {
return null;
}
// 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);
const compareFnChanged = compareFn !== this.compareFn;
if (differChanges) {
this.keyValues = [];
differChanges.forEachItem((r: KeyValueChangeRecord<K, V>) => {
this.keyValues.push(makeKeyValuePair(r.key, r.currentValue!));
});
}
if (differChanges || compareFnChanged) {
if (compareFn) {
this.keyValues.sort(compareFn);
}
this.compareFn = 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 both keys are the same, return 0 (no sorting needed).
if (a === b) return 0;
// If one of the keys is `null` or `undefined`, place it at the end of the sort.
if (a == null) return 1; // `a` comes after `b`.
if (b == null) return -1; // `b` comes after `a`.
// If both keys are strings, compare them lexicographically.
if (typeof a == 'string' && typeof b == 'string') {
return a < b ? -1 : 1;
}
// If both keys are numbers, sort them numerically.
if (typeof a == 'number' && typeof b == 'number') {
return a - b;
}
// If both keys are booleans, sort `false` before `true`.
if (typeof a == 'boolean' && typeof b == 'boolean') {
return a < b ? -1 : 1;
}
// Fallback case: if keys are of different types, compare their string representations.
const aString = String(a);
const bString = String(b);
// Compare the string representations lexicographically.
return aString == bString ? 0 : aString < bString ? -1 : 1;
}