angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
95.93k stars 25.36k forks source link

Add support for using functions as pipes #55471

Open daniel-seaton opened 5 months ago

daniel-seaton commented 5 months ago

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

Currently, in order to create a custom pipe, there is a good amount of boiler-plate code. You need to add the @Pipe decorator to an entire class that you create, just so you can extend PipeTransform and implement what often ends up being a small transformation or piece of logic.

This is reminiscent of the old CanActivate(Children) and CanDeactivate(Children) classes, which were deprecated in favor of Can(De)ActivateFn. I am suggesting making a similar change for pipes, which would allow devs to reduce the amount of boilerplate code they have to write as well as allowing devs to keep small or extremely specific pipes within the class files they're used, since the function could just be defined on the class.

Proposed solution

Creating a PipeFn class which defines the inputs/outputs required for a function to work as a pipe, and allow functions to be used as a pipe in lieu of entire classes.

Alternatives considered

An alternative would be modifying the @Pipe decorator so that it can be used on classes or functions, or creating a different decorator for functions to mark them as pipes.

pkozlowski-opensource commented 5 months ago

Prior discussion: #24559

pkozlowski-opensource commented 5 months ago

Currently, in order to create a custom pipe, there is a good amount of boiler-plate code. You need to add the @Pipe decorator to an entire class that you create, just so you can extend PipeTransform and implement what often ends up being a small transformation or piece of logic.

tl;dr; I absolutely do share the sentiment, although we need to dig deeper to see where pipes are really useful and creating them is worthwhile. For some cases, though, creating a pipe is just a pure overhead and one should use a function or a computed instead.

Pipes - the good parts

While the transform function is the hart of every pipe, this is ... just a function and the real benefits of pipes are:

The pipes are brilliant for library authors that want to create fairly complex pipes with dependencies, especially pure pipes.

Pipes - the alternatives

Pipes without constructors

Non-pure pipes

First of all, if a given pipe doesn't have any constructor and is not pure there is not much reason for it to be a pipe - just create a regular TypeScript function.

Pure pipes

The main benefit of pure pipes without a constructor is their memoization capability. I do very much agree that in this particular case a function as a pipe would be very useful.

For applications using signals a computed might be a good alternative, though.

Pipes with a constructor

Pipes that do have a constructor indicate that there is some creation / initialisation logic and in this sense having a class for a pipe is useful. We could use a factory function instead but this is just introducing a different way of doing things without clear benefits.

Pipes as functions

Given the above discussion we can see that registering functions as pipes would be mostly beneficial for pure pipes without a constructor. So if we go down this path it would cover only a subset of use-cases and introduce 2 way of doing things.

But assuming that we do want to go down this path, let's look at the possible API designs

Technical solutions

Decorators

In #24559 the suggestion was to use a decorator on a function, which is, sadly, not supported in TS / upcoming TC39 spec.

Instance methods

This issue suggest using a directive's instance members as a pipe. This is challenging since there is no place to specify pipe's metadata - if it is pure or not. This could be done with a decorator, ex.:

@Component({
    ...,
    template: `{{'hi' | upper}}`
})
class MyComponent {
   @Pipe({pure: true})
   upper(value: number): string {
        return value.toUpperCase();
   }
}

The drawback here is that those pipes are tied to the instance of a given component and are no longer easy to share.

pkozlowski-opensource commented 5 months ago

Given the background in my previous comment, I'm not sure if there is enough benefit of implementing an alternative syntax for pipes.

What we really seem to want here is the memoization aspect of pipes but we do have signals now.

alxhub commented 5 months ago

There is also the question of #43485 - pipes are currently a completely custom syntax. If we move in the direction of making Angular expressions just plain TypeScript, clearly pipes don't fit into that story.

JoostK commented 5 months ago

One downside of computeds is how they need to be stored somewhere, i.e. they can't be temporaries. That can be a limitation in e.g. @for-loops, where pipes help perform ad-hoc transforms where the template (or more accurately, the embedded view) acts as the storage location.

An idle thought could be to have some syntax to declare a computed within the template, such that computeds can be declared in the context of an embedded view.

Harpush commented 5 months ago

For now we created a memoize pipe that accepts a function and runs it. So the syntax is: someFn | memoize:arg1:arg2. someFn must be pure and doesn't have access to this. It mostly answers our needs.

I do think what @JoostK said is a good idea though I am not sure how to do it and whether it is actually a part of template local variables (another issue)

hakimio commented 5 months ago

ngxtension callPipe / ApplyPipe can be used as a workaround. Also, @memoize() decorator from utils-decorators or Taiga UI @tuiPure decorator can be used to memoize the function.