Skip to content
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

Add support for using functions as pipes #55471

Open
daniel-seaton opened this issue Apr 22, 2024 · 7 comments
Open

Add support for using functions as pipes #55471

daniel-seaton opened this issue Apr 22, 2024 · 7 comments
Labels
area: core Issues related to the framework runtime core: pipes
Milestone

Comments

@daniel-seaton
Copy link

daniel-seaton commented Apr 22, 2024

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
Copy link
Member

Prior discussion: #24559

@pkozlowski-opensource pkozlowski-opensource added area: core Issues related to the framework runtime core: pipes labels Apr 24, 2024
@ngbot ngbot bot modified the milestone: needsTriage Apr 24, 2024
@pkozlowski-opensource
Copy link
Member

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:

  • pipes have a constructor which allows them to inject dependencies and execute logic at the time a given expression containing a pipe is evaluated for the first time; this is a very unique and handy... if one needs a creation time dependencies / logic;
  • pure pipes are memoized;
  • pipes provide some structure to create and re-use sharing logic.

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
Copy link
Member

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
Copy link
Member

alxhub commented Apr 24, 2024

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
Copy link
Member

JoostK commented Apr 24, 2024

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
Copy link

Harpush commented Apr 25, 2024

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
Copy link

hakimio commented May 1, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: pipes
Projects
None yet
Development

No branches or pull requests

6 participants