angular / angular

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

[Feature request]: creating a local pipe in component for template #25976

Closed splincode closed 2 years ago

splincode commented 6 years ago

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[ ] Performance issue
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
[ ] Other... Please describe:

Current behavior

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    original: {{ msg }} 
    reverse: {{ msg | reverse }}   
  `
})
export class AppComponent  {
  public msg = 'Angular';
}

reverse.pipe.ts

import { Pipe } from '@angular/core';

@Pipe({name: 'reverse'})
export class ReversePipe implements PipeTransform {
  public transform(value: string): string {
    return value.split('').reverse().join('');
  }
}

Expected behavior

I. First expected

import { PipeLocal, Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    original: {{ msg }} 
    reverse: {{ msg | reverse }}      
  `
})
export class AppComponent  {
  public msg = 'Angular';

  @PipeLocal()
  public reverse(value: string) {
    // pure invoke function in template
    return value.split('').reverse().join('');
  }

}

II. Second expected

import { Pipe } from '@angular/core';

@Pipe({
  name: 'reverse', 
  provideIn: 'root'
})
export class ReversePipe implements PipeTransform {
  public transform(value: string): string {
    return value.split('').reverse().join('');
  }
}

I would also like to have provideIn for pipe. Because I can have many utilities and every time I import them tires.

What is the motivation / use case for changing the behavior?

original: {{ msg }}
reverse: {{ reverse(msg) }}

@StephenFluin @mhevery @trotyl Do you think this is possible?

mhevery commented 6 years ago

@robwormald this is similar to what we have discussed.

lazarljubenovic commented 6 years ago

I'd like to see local pipes as well, but I disagree with the rationale -- it seems to suggest that pipes are the way to use pure functions in templates to prevent rapid invocation on every CD cycle. Pipes are pure, yes, but that doesn't mean that everything that we want to be pure should be solved by a pipe. Pipes should be used for end-user-friendly formatting of data (decimal pipe, date pipe, currency pipe), not for arbitrary invocation of pure functions.

The way I see it, this proposal asks about a way to define pure functions, but (wrongly, in my opinion) assumes that pipes are the way to go.

Instead of pipes, why don't we allow this:

  @Pure()
  public reverse(value: string) {
    return value.split('').reverse().join('')
  }

Then, in the template, we can just do, as always,

{{ reverse(msg) }}

but without the degrading performance implications.


When looked from this perspective, I don't think that you often actually want a local pipe. A pipe should be universal and should be able to format data from everywhere in the app -- there should be little reason to tie it to one specific component. It's not pipes that we need, it's pure functions.

mlc-mlapis commented 6 years ago

A pipe should be universal and should be able to format data from everywhere in the app

Sure, but still there are many cases (at least at our environment) where local pipes are useful because they are exceptions for just that one component ...

@Pure() public reverse(value: string) { ... }

This way is certainly the preferred one but we have to also answer what should be the limits for input params ... the same logic as for OnPush strategy? So immutable objects, arrays + primitives?

lazarljubenovic commented 6 years ago

what should be the limits for input params

The same as with pipes, I guess?

pkozlowski-opensource commented 5 years ago

I think that @lazarljubenovic nailed it in his comment:

I'd like to see local pipes as well, but I disagree with the rationale -- it seems to suggest that pipes are the way to use pure functions in templates to prevent rapid invocation on every CD cycle. Pipes are pure, yes, but that doesn't mean that everything that we want to be pure should be solved by a pipe. Pipes should be used for end-user-friendly formatting of data (decimal pipe, date pipe, currency pipe), not for arbitrary invocation of pure functions.

Going over this issue (and similar ones) I've realised that we are discussing 2 separate issues:

  1. ability to define and use a pipe in one component only (without a need of registering it in NgModule, sharing with other components etc.); the need here is to simplify pipes creation, especially when those are used in one component only;

  2. make sure that (pure) functions are not repeatedly called in each and every change detection cycle; the need here is to avoid unnecessary work and (potential) performance issues.

From what I'm reading in other issues the (2) is what we are really after and (1) is just a mean to getting (2). While I agree that pure pipes could be used to limit unnecessary invocations of (pure) functions it sounds like we are after property of pure pipes (memorisation of results based on arguments) here. Using pure pipes could work but IMO it is unnecessary complication (one needs to create a class, register it in NgModule, Angular would have create its instance, we need to take care of the this etc.) for simply telling Angular "hey, this is a pure function, don't call it repeatedly if arguments are the same".

I totally see the need of better support for pure functions in Angular template syntax but as @lazarljubenovic I don't believe that pipes are the way to go. It would be just too heavy for what we really need.

pkozlowski-opensource commented 5 years ago

BTW, the (1) need from the above discussion is a duplicate of (or at least very much related to) #24559

manklu commented 5 years ago

2. make sure that (pure) functions are not repeatedly called in each and every change detection cycle; the need here is to avoid unnecessary work and (potential) performance issues

Solvable with Observables. It's just a matter of getting used to it.

BAM5 commented 5 years ago

Just thought of this elegant little solution to the "pure function" issue. It's a good workaround for now. Actually, now that I think about it it somewhat solves the whole feature request since with this method you can now defer a pipe call to whatever function you want. You could even modify the pureCall pipe so it will defer to a class instance generated by the passed function if you needed a stateful transform pipe (although that would most likely defeat the whole purpose of making it a pipe since stateful transform sounds like "impure" to me).

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'pureCall' })
export class PureCallPipe implements PipeTransform {
    transform(func:Function, ...args:any[]):any {
        return func(...args);
    }
}

And can be used like so

@Component({
    selector: 'app-root',
    template: `<ul><li *ngFor="let hash in imageHashes">{{hashToFile|pureCall:hash:'frank'}}</li></ul>`,
})
export class AppRoot{
    imageHashes:string[] = ['fakehash', 'stillfake', 'whyareyoulookinghere-stillsuperfake'];

    hashToFile(imageHash:string, name:string):string{
        return `/user/${name}/${imageHash}.jpg`;
    }
}

Enjoy!

Airblader commented 5 years ago

Pipes are pure, yes, but that doesn't mean that everything that we want to be pure should be solved by a pipe. Pipes should be used for end-user-friendly formatting of data (decimal pipe, date pipe, currency pipe), not for arbitrary invocation of pure functions.

IMHO pipes have another major benefit, which is centralizing formatting logic for reusability. In our applications we have a lot of enums, and enums want to be displayed in a readable format or be mapped to a specific icon etc. Having pipes for these makes it very easy to manage this logic separately and DRY. The same could be achieved by services, but that would require manually injecting them everywhere.

→ I agree that not every single pure function should be a pipe, but I do think the market for pipes is or can be bigger than it seems.

lazarljubenovic commented 5 years ago

I like the idea with icons, since an icon is still a formatting issue. You don't need the icon in the model, you only want to display it to the user from the template. Just like you transform 10000 into 10.000,00, you also want to transform true into a green-colored circle and false into a red-colored circle. So I see <my-icon [icon]="user.isOnline | iconize"></my-icon> conceptually equivalent to {{ user.postsCount | number : '1.0-0' }}, even though one pipe returns a string and the other pipe returns (probably) and object with SVG data attached to it, and my-icon knows how to turn this data into a valid HTML/SVG structure.

MaximSagan commented 4 years ago

I hate to be that guy, but... any progress on this issue?

SuperiorJT commented 4 years ago

Currently working on something in my app that would be improved by a feature like this. I have a component that displays translated tooltip content based on the entered data. I think if you guys do plan to implement something like this to also consider idempotent functions rather than just pure functions. This applies in my case because I have to use a service to translate the result, but the result is consistently the same consistently with a given input.

I guess if it were impossible to handle idempotent functions, then I can pass the result of the pure function to a translate pipe. For other situations like this though, I would prefer to be able to do everything without a pipe if the function is available in the component.

pkozlowski-opensource commented 2 years ago

Standalone components support locally-defined pipes, ex.:

@Pipe({ name: 'brackets', standalone: true })
export class ExamplePipe implements PipeTransform {
  transform(value: any, ...args: any[]) {
    return `(${value})`;
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ExamplePipe],
  template: `{{'I am in brackets' | brackets}}`,
})
export class ExampleStandaloneComponent {}

Working stackblitz: https://stackblitz.com/edit/angular-standalone-ks9pkt?file=src%2Fmain.ts

Closing as already implemented.

evgeniyefimov commented 2 years ago

Standalone components support locally-defined pipes, ex.:

@Pipe({ name: 'brackets', standalone: true })
export class ExamplePipe implements PipeTransform {
  transform(value: any, ...args: any[]) {
    return `(${value})`;
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ExamplePipe],
  template: `{{'I am in brackets' | brackets}}`,
})
export class ExampleStandaloneComponent {}

Working stackblitz: https://stackblitz.com/edit/angular-standalone-ks9pkt?file=src%2Fmain.ts

Closing as already implemented.

I think it's not what people from this topic expected. We could do almost the same without standalone components. This is not ergonomic solution.

pkozlowski-opensource commented 2 years ago

@evgeniyefimov the problem is that there are multiple items being discussed here so at this point it is not very clear what the expectation is.

But if people want to use "just a function in a template":

Is there more to it?

evgeniyefimov commented 2 years ago

@evgeniyefimov the problem is that there are multiple items being discussed here so at this point it is not very clear what the expectation is.

But if people want to use "just a function in a template":

Is there more to it?

@pkozlowski-opensource Call a function in a template is a bad pratice. It'll be called each change detection cycle, but pipe only when its' inputs are changed. About memoized functions @dawidgarus mentioned here https://github.com/angular/angular/issues/20419#issuecomment-425070292 that memoized function can't do things like: <div *ngFor="let item of list">{{func(item, arg2)}}</div>. There is a discussion about this.

Would be great to have some really simple solution, just an example:

@Component({
...
})
export class Component {
  sum(a: number, b: number): number {
    return a + b;
  }
<div>{{ sum | a : b }}</div>
lazarljubenovic commented 2 years ago

It's a bad practice to do it mindlessly. There are countless cases where it's perfectly fine to do so and is no better than a pipe.

angular-automatic-lock-bot[bot] commented 2 years ago

This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.