-
Notifications
You must be signed in to change notification settings - Fork 24.8k
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
Feature: Introduce *call* pipe #20419
Comments
It would mean that your template has to know about your implementation details. |
@vicb Why would You need to know implementation details? All you need to know is that there is a function class MyComponent {
arg1: any;
arg2: any;
funcResult: any;
private arg1Copy: any;
private arg2Copy: any;
ngDoCheck(): void {
if (this.arg1 !== this.arg1Copy || this.arg2 !== this.arg2Copy) {
this.arg1Copy = this.arg1;
this.arg2Copy = this.arg2;
this.funcResult = this.func(arg1, arg2);
}
}
} And that's just for one function call. What if the were more? Observables really don't make it easier, unless you already have observables for <div *ngFor="let item of list">{{func(item, arg2)}}</div> |
this makes a lot of sense for component logic that needs to be run in the template and in the component class. component@Component({...})
export class SomeComponent implements OnInit {
numbers = [1,2,3,4,5,6];
ngOnInit() {
numbers.filter(isEven).forEach( console.log)
}
isEven(value: number) {
return value % 2 === 0;
}
} template<ul>
<li *ngFor="let item of numbers" [blue-background]="item | callback : isEven"></li>
</ul> This is a trivial example, but the alternative is to create many more specialized pipes, Then either keep the logic in a service that both the component and the pipe ingest, or import the pipe and run its logic with .transform. Sometimes components have unique logic that doesn't need to be abstracted into a service and forcing it into a service or a pipe just makes unnecessary abstraction. It a little weird until you know about it, but this wouldn't be the first instance of that in Angular ;) |
I fleshed out a stackblitz to try and demonstrate the utility and performance benefits of this implementation. I would also like to point to the end of an otherwise very good article on Angular best practices, where the author advocates for functions in the template. He demonstrates in other articles how using pipes is a better option, but a lot of beginning courses teach this method as well, then later the dev learns that it's bad practice. This pipe could lower the learning curve on implementing functions in templates in a performant manner. |
A more dynamic approach would be to use spread on args, which will allow the consumer to use any functions with a dynamic set of arguments
template:
ts:
Not sure if I think this should go into the library though - it's very simple to implement this yourself, and the stack traces are easier to debug if you have everything in your own code (imo). |
@yusijs ... yep, easy enough but the problem is that a lot of Angular devs today still don't see/understand/know about this way and why it is so useful, so adding this as a core feature can help them a lot. |
As an alternative approach, new feature could be introduced: class MyComponent {
@PipeFunction()
myPipe(value, ...args) { // method must be compatible with PipeTransform.transform
// do something
}
} it could be used in template as regular pipe: |
@dawidgarus ... I like the name |
@mlc-mlapis If the compiler has to be changed either way, wouldn't it be better to keep the normal syntax for functions and just mark them export class MyComponent {
public foo = 1;
public bar = 4;
@Pure()
public add(a: number, b: number): number {
return a + b;
}
} <span>{{ add(foo, bar) }}</span> |
@trotyl computed properties can't do this: |
@trotyl You could say that, but while the end result is mostly the same, it's different in a way that arbitrary properties aren't tracked, only the input values - and that mechanism is already implemented for pipes. For example, in the following scenario, the export class MyComponent {
public foo = 1;
public bar = 4;
@Pure()
public add(a: number, b: number): number {
return a + b + this.baz;
}
private baz = 9;
} <span>{{ add(foo, bar) }}</span> |
@skreborn ... you described exactly what I intended by that |
@dawidgarus With observed computed properties, there could be different paradigm like: <div *ngFor="let item of items; index as i">{{ items[i] }} class MyComponent {
@Input() list: any[]
@Computed()
get items() {
return this.list.map((item) => this.func(item, arg2))
}
} I agree it not really duplicate for the preference difference, but would both achieve the goal of being performant.
@skreborn The export class MyComponent {
@Observe() foo = 1;
@Observe() bar = 4;
@Computed()
get sum(): number {
return this.a + this.b + this.baz;
}
private baz = 9;
} |
@trotyl That is exactly the intention. If the values are accessed via |
@trotyl It's still different functionality which forces you to make conceptual changes in code and fell like a weird workaround. For every example you give with computed properties I can give another how it's different. How about: |
@yusijs the implementation of introducing arguments is arbitrary. I realize spreading an array is the "Angular way" but I personally don't like to marry function parameters to indices in an array, I think a config object of key values is cleaner. Though, I don't really care how arguments are passed. Additionally, it being easy to implement has nothing to do with it. The Also, your implementation is different from my proposal. You are piping the function and passing arguments. I am piping the value into a function which is the first argument of the callback pipe, additional arguments can follow. These should always be pure functions that don't manipulate the component values. I would be happy with the |
@dawidgarus Then you'll have something like: <ng-container *ngFor="let item of items">
<div *ngIf="item[0] | async">{{ item[1] | async }}</div>
</ng-container> class MyComponent {
@Input() list: any[]
@Computed()
get items() {
return this.list.map((item) => {
const tmp = this.getData(item).pipe(publish())
return [tmp, tmp.pipe(map(_ => this.func(_, arg2)))
})
}
} Anything can be done in template can be done in logic, it’s basic theory, but template could save more and more time when goes complicated. |
Exactly. It get's complicated because it's different feature. It's like using a cow to mow the lawn - you can do it, but that's not the purpose of the cow. Imagine doing code review of your snippet and you are like: who wrote this little monster? |
here is a stackblitz with a bunch of different implementations and comparisons of functions directly in template vs pure pipe vs impure pipe. I have finished about 80 percent of the code for a PR if the teams sees the need for it. Users can easily watch one or multiple values for changes or pass multiple arguments to pipe. I would like to hear what others have to say about I personally think that @jelbourn I'm sure you're busy but I would love to get something like this into core and am happy to do all the work. I would just like to know how likely it is to be accepted before putting in the time. EDIT: I should have called out @vicb as he was already on this issue. Does this seem like a viable solution, or are you guys going down the |
Seems like discussion died down a while ago. Wondering if a pipe like this would be worth introducing as a core pipe: import { Pipe, PipeTransform } from '@angular/core';
// https://github.com/ArtemLanovyy/ngx-pipe-function
@Pipe({
name: 'variadicFunctionPipe',
})
export class VariadicFunctionPipe implements PipeTransform {
public transform<Input extends any[], Output>(
values: Input,
handler: (...values: [...Input]) => Output,
context?: unknown
): Output {
if (context) {
return handler.call(context, ...values);
}
return handler(...values);
}
} with usage like @Component({
selector: 'demo-component',
template: `
{{ [firstName, lastName] | variadicFunctionPipe: fullName }}
`,
})
export class DemoComponent {
@Input() public firstName = 'David';
@Input() public lastName = 'Shortman';
public fullName(firstName: string, lastName: string): string {
return `${firstName} ${lastName}`;
}
} |
If I were to take a view, I would say that supporting a The Furthermore, IVY already has support for this internally with its |
Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends. Find more details about Angular's feature request process in our documentation. |
From https://github.com/nigrosimone/ng-generic-pipe (npm https://www.npmjs.com/package/ng-generic-pipe) import { ChangeDetectorRef, EmbeddedViewRef, Type } from '@angular/core';
import { Pipe } from '@angular/core';
import { PipeTransform } from '@angular/core';
@Pipe({
name: 'ngGenericPipe',
pure: true
})
export class NgGenericPipe implements PipeTransform {
private context: any;
constructor(cdRef: ChangeDetectorRef) {
// retrive component instance (this is a workaround)
this.context = (cdRef as EmbeddedViewRef<Type<any>>).context;
}
public transform(
headArgument: any,
fnReference: any,
...tailArguments: any[]
): any {
return fnReference.apply(this.context, [headArgument, ...tailArguments]);
}
} usage import { Component } from '@angular/core';
import { Observable, of } from 'rxjs';
@Component({
selector: 'app-root',
template: `{{ name | ngGenericPipe: testFromPipe:"arg1":"arg2"}}`,
})
export class AppComponent {
public name: string = 'foo';
testFromPipe(a: string, b: string, c: string): string {
console.log(a, b, c);
}
} |
This comment was marked as abuse.
This comment was marked as abuse.
Looking into this (and similar) issues it sounds like we are looking for using a pipe as something akin to computed properties. We do consider a more generic solution. Since we are getting multiple issues / feature requests around computed properties, we've opened a canonical issue #47553 to track this use-case. Currently there is no exact solution / design, but we do acknowledge the use-cases and the need for such concept. |
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. |
I'm submitting a...
Current behavior
Using function in template like this
{{func(arg1, arg2)}}
would cause function call on every change detection, even when the arguments didn't change.One could write pure pipe to avoid it, but it's a tedious job when You have a lot of relatively simple functions that are used only in one component and it pollutes module namespace.
Expected behavior
call
pipe should be introduced, which would call a function with arguments, like this:{{func | call:this:arg1:arg2}}
.call
should be pure, so it would be called only when an argument changes.What is the motivation / use case for changing the behavior?
Less code without unnecessary pipes created to be used only once or in one component.
The text was updated successfully, but these errors were encountered: