-
Notifications
You must be signed in to change notification settings - Fork 26.2k
feat(common): introduce KeyValuePipe #24319
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
Conversation
value: V; | ||
} | ||
|
||
@Pipe({name: 'mutableToKeyValue', pure: false}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don' think the word mutable
should be there. It is inconsistent with the rest of the systems naming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to keyvalue
similar to the lowercase
on LowerCasePipe
this.differ = this.differs.find(input).create(); | ||
} | ||
|
||
// TODO: shouldnt the differ allow Map? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does: https://github.com/angular/angular/blob/master/packages/core/src/change_detection/differs/default_keyvalue_differ.ts#L74 Not sure I understand that comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get a type error if I pass input
directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
specifically:
Argument of type '{ [key: string]: V; [key: number]: V; } | Map<K, V>' is not assignable to parameter of type '{ [key: string]: V; }'.
Type 'Map<K, V>' is not assignable to type '{ [key: string]: V; }'.
Index signature is missing in type 'Map<K, V>'
let nextValue: Array<KeyValuePair<string, V>>; | ||
// always return a new array ref at this | ||
if (input instanceof Map) { | ||
// keys maintain their type here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should be stringifing it here. What is your reasoning for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object.keys converts all keys to strings. So to be consistent we should do the same with Map
.map(key => this.makeKeyValuePair(String(key), input.get(key))); | ||
} else { | ||
// all keys will be converted into strings when using Object.keys() | ||
nextValue = Object.keys(input).sort().map(key => this.makeKeyValuePair(key, input[key])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map
is not very performant. Given that this will be in the hotpath could we implement it with a for loop instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Still not overly happy with Array.from(input.keys())
since it's an extra iteration but I don't see another way to sort it without resolving the iterator into an Array
|
||
import {KeyValueDiffer, KeyValueDiffers, Pipe, PipeTransform} from '@angular/core'; | ||
|
||
export interface KeyValuePair<K, V> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think KeyValue
is shorter. I don't think the word Pair
adds value.
transform(input: null | { | ||
[key: string]: V; | ||
[key: number]: V; | ||
} | Map<K, V>): Array<KeyValuePair<string, V>> | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect that KeyValuePair
should be exported as well since it would be part of the public API, but I don't see it in this file.
packages/common/src/pipes/index.ts
Outdated
@@ -14,6 +14,7 @@ | |||
import {AsyncPipe} from './async_pipe'; | |||
import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from './case_conversion_pipes'; | |||
import {DatePipe} from './date_pipe'; | |||
import {MutableToKeyValuePipe} from './entries_pipe'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect KeyValuePair
to be exported as well since it is part of public API.
@mhevery I've addressed your feedback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sorry I missed the docs the first time through. Hopefully this is the last pass.
} | ||
|
||
@Pipe({name: 'keyvalue', pure: false}) | ||
export class KeyValuePipe<K, V> implements PipeTransform { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I did not notice this earlier. Because this is public API it needs to have documentation. See example here: https://github.com/angular/angular/blob/master/packages/common/src/pipes/date_pipe.ts#L14-L105
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never been very good at docs, let me know if that'll suffice
return nextValue; | ||
} | ||
|
||
private makeKeyValuePair(key: number|string, value: any): KeyValue<string, V> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method would be better as free floating function since it would minify better. (It does not use this
hence it does not need to be a member of the class)
import {KeyValueDiffer, KeyValueDiffers, Pipe, PipeTransform} from '@angular/core'; | ||
|
||
export interface KeyValue<K, V> { | ||
key: K; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since K
is always a string
there is no need for K
let nextValue: Array<KeyValue<string, V>> = []; | ||
|
||
if (input instanceof Map) { | ||
const keys = Array.from(input.keys()).sort(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If keys
are not string
s than the sort
will fail. I think you need to convert to string
before sort
()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless i've misinterpted you, sort can handle number
just fine.
take this example
The reason im hesitant to simply convert it is that it'll be a whole extra iteration.
Also I do have a test for this https://github.com/angular/angular/pull/24319/files#diff-b6bf05a02506b3cd96c753d3a8d0d30aR72
maybe I should add one with alpha and numerical
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I get what you mean: when the input is a function, object etc.
I think adding a param for a sort comparitor function would be the best idea here. With the default behaviour being .sort()
so leave it to the dev to add the comparitor if they have complex keys
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
// input could possibly be Megamorphic here | ||
nextValue.push(this.makeKeyValuePair(String(key), input[key])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to convert to string
since Object.keys
will always return strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, missed that in refactoring
A question before this becomes public API: shouldn't the sort be optional? (maybe on by default, but configurable at least?) |
@mhevery I've added a compareFn to deal with the sorting issues but i've become a bit uncertain about the implementation now. |
* 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. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
this.differ = this.differs.find(input).create(); | ||
} | ||
|
||
// TODO: shouldnt the differ signature allow Map? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldnt
=> shouldn't
The issue is that you declared your type as {} | Map
where as KeyValueDiffer
has: diff(object: Map<K, V>): KeyValueChanges<K, V>;
and diff(object: {[key: string]: V}): KeyValueChanges<string, V>;
overloaded methods. The issue is that neither {}
or Map
is a subtype of {}|Map
. To make this work you would have to declare diff(object: {[key: string]: V}|Map<K,V> ): KeyValueChanges<K|string, V>;
or something like that, but than you would love the type information on string
.
So casting to any
is a reasonable compromise.
Alternatively you could do:
const differChanges = input instanceof Map ? this.differ.diff(input) : this.differ.diff(input);
Which types but is very weird.
I would remove the comment/TODO.
return {key: key as any, value}; | ||
} | ||
|
||
export interface KeyValue<V> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
KeyValue is part of public API, it needs docs. (example not needed)
OK, I thought about it some more and I have done some refactoring to clean up the code and the type system. I pushed the changes here 97fbe77 |
@mhevery Thanks man, that helps a lot, will make those final few docs and tests tonight |
* | ||
*/ | ||
@Pipe({name: 'keyvalue', pure: false}) | ||
export class KeyValuePipe implements PipeTransform { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@mhevery Addressed all your feedback and added more tests / docs. The sourcelab failure looks unrelated to my changes |
@Toxicable Thank you for all of your patience in getting this into mergeable state. 👍 |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
There is no way to iterate a object or Map
Closes: #24200
Continuation from: #11319
What is the new behavior?
A pipe to object Map or object (dictionary) to a key value pair array for use in a ngFor
Does this PR introduce a breaking change?