angular icon indicating copy to clipboard operation
angular copied to clipboard

Support arrow functions in template syntax

Open jelbourn opened this issue 8 years ago • 31 comments

It's not uncommon for a component to take as input a transformation function or predicate function.

For example, this PR for @angular/forms adds an @Input for a predicate function for determining if two options in a <select> are equal. Right now, this looks something like:

<select [compareWith]="equals" [(ngModel)]="selectedCountries"> ...

equals = (a: Country, b: Country) => {
   return a.id === b.id;
};

With this feature, this could be written as

<select [compareWith]="(a, b) => a.id == b.id" [(ngModel)]="selectedCountries"> ...

There are other places in Angular Material where we're looking to add similar API, e.g., datepicker with something like:

<md-datepicker [allowedDateFilter]="d => isWeekend(d)">

Or autocomplete, where the component needs to take an arbitrary value and know what string to write into the text input:

<md-autocomplete [displayWith]="value => value.getFullName()">

This would have to account for:

  • Functions with multiple arguments ((a, b) => a + b)
  • Functions with param names that already exist in the context (<my-x #x [uniq]="x => x.id">)
  • Functions that return an object literal (x => ({name: x}))
  • Invoking functions on the context (x => isActive(x)
  • Passing functions through a pipe (x => x.activate() | debounce)

FYI @mhevery for planning

jelbourn avatar Jan 27 '17 00:01 jelbourn

Thought: we could support x => {name: x} (without required "()") as we won't support fn body.

vicb avatar Jan 27 '17 00:01 vicb

In AngularJS, we used to recommend against adding too much logic in the templates. For example:

[...] The reason behind this is core to the AngularJS philosophy that application logic should be in controllers, not the views. If you need a real conditional, loop, or to throw from a view expression, delegate to a JavaScript method instead.

Is this less of a concern in Angular now?

I know this is meant for good and being able to (a, b) => a + b or x => x.someMethod() can come very handy (and doesn't include too much logic), but I am afraid people will end up doing things more like:

[displayWith]="x =>
  haveASideEffect(x) && 
  callMyApi({lastValue: x}) && 
  doSomethingElse() && 
  x.getFullName()"

gkalpak avatar Jan 27 '17 11:01 gkalpak

@gkalpak My opinion on this one is that while we do want to discourage complex logic in the template, ultimately the template syntax exists in the first place to be a convenience and improve developer productivity. This feature makes it simple and clean to capture a somewhat common use-case, especially for functional-style programming, which is worth the fact that some people may go overboard with it.

jelbourn avatar Jan 27 '17 17:01 jelbourn

Found the issue because I needed it to have thunks in my templates.

<comp *structural="() => some.undefinedValueAtViewInit"></comp>

So I think there are valid use cases (mine may seem strange but I can assure you that it makes sense with the thing I'm working on). But I agree with @gkalpak about potential abuses. Personally, I've not been surprised when the JiT compiler blown up when I tried to use an arrow function.

But still, that would be a nice and neat feature to have for responsible developers.

toverux avatar Oct 28 '17 23:10 toverux

As a related issue, in Material 2 Autocompleter, the displayWith function is not binded to the Component, and I was not able to access the internal variables that I require to figure out the result.

I figured out a workaround that worked, to keep my logic in the Component:

displayFn() {
    return (value: StateOption): string => {
      return !value ? '' : value.viewValue[this.lang];
    };
  }

then "inject" it into the template:

<mat-autocomplete [displayWith]="displayFn()" #states="matAutocomplete">

Without wrap the arrow function, this.lang is undefined :(

matheo avatar Dec 08 '17 18:12 matheo

@matheo you can use .bind(this):

component.ts

displayFn(value: StateOption): string {
    return !value ? '' : value.viewValue[this.lang];
}

component.html

<mat-autocomplete [displayWith]="displayFn.bind(this)" #states="matAutocomplete">

this is in reference to the component instance.


Still would like to see this inlined arrow feature though.

mtraynham avatar Dec 21 '17 15:12 mtraynham

Is there any update?

antontrafimovich avatar Oct 26 '18 10:10 antontrafimovich

Is there any way to make a workaround for this? Something like a Pipe that supports multiple parameters and returns a function?

frankiDotNet avatar Jan 29 '19 07:01 frankiDotNet

@Franki1986: as far as I could tell, there are only two ways to get this:

  1. Going for @jelbourn syntax. The function in the component has to be declared like equals = (a: Country, b: Country) => and not function equals(a: Country, b: Country) { or you'll lose this.
  2. Declare the function normally and then inject this with bind, just like @mtraynham said. But this feels so 2014...

Guatom avatar Feb 26 '19 17:02 Guatom

Pretty please 👍 🦄 especialy the displayWith usecase sucks 🤢 Maybe Ivy can handle this?

gotwig avatar Mar 07 '19 15:03 gotwig

with myfn.bind(this) and multiple emitters (like in a list) you run into trouble, best to use myfn = () => {} to retain a valid reference to this (instead of SafeSubscriber)

mjolk avatar Apr 08 '19 13:04 mjolk

@matheo you can use .bind(this):

component.ts

displayFn(value: StateOption): string {
    return !value ? '' : value.viewValue[this.lang];
}

component.html

<mat-autocomplete [displayWith]="displayFn.bind(this)" #states="matAutocomplete">

this is in reference to the component instance.

Still would like to see this inlined arrow feature though.

It works perfectly well enough, but my Visual Studio Code error checking reports "Unknown Method 'bind' " :)

Is it possible to do this kind of .bind() in the *.ts file, instead?

EliezerB123 avatar Jul 28 '19 13:07 EliezerB123

Is it possible to do this kind of .bind() in the *.ts file, instead?

Of course, just do it in the constructor. This will have no impact on type-checking. As bind creates a new function, you also need to reassign it:

public constructor() {
    this.displayFn = this.displayFn.bind(this);
}

toverux avatar Jul 28 '19 14:07 toverux

Ran into the need of an arrow function for a Component Input just now and found this issue. I simply wanted to inject some behaviour in another component. A sytax like this would be nice:

<module-title [helpAction]="() => helpModal.open()"></module-title>

Edit: Yes, yes, I know. I could do this:

<module-title [helpAction]="getOpenHelpModalCallback()"></module-title>

. . .

export class ModuleTitleComponent  {
. . .
    getOpenHelpModalCallback(): () => {} {
        return () => this.helpModal.open();
    }
}

But, I find it bothersome.

ghost avatar Jan 23 '20 16:01 ghost

@jotiheranbnach Yeah, it's just extra, redundant code. That's a major reason to have this feature. I like to put simple stuff like this in the template, both to get rid of unnecessary code (keeping it DRY and KISS) and because it's easier to read, you don't have to go to keep going back to the component to see what each function is really doing.

kesarion avatar Jan 23 '20 17:01 kesarion

+1 for usage with matAutocomplete.[displayWith]. This seems very common to be working with objects in memory, but wanting to pluck an attribute for template interpolation.

evictor avatar Jan 13 '21 22:01 evictor

Any updates on this?

haskelcurry avatar Apr 09 '21 09:04 haskelcurry

Any updates on this?

PeterChen1997 avatar Apr 12 '21 08:04 PeterChen1997

If there are any updates you'll see them here or on the team's roadmap.

jelbourn avatar Apr 13 '21 22:04 jelbourn

Maybe one more use case that I stumbled upon today:

When work with observables and the async pipe, I want to do something like this to avoid subscribing in the Component code:

<ng-container *ngIf="(availableAppointments$ | async) as availableAppointments">
    <ngb-datepicker [markDisabled]="(event) => isDateDisabledCallback(availableAppointments, event)">

MetroMarv avatar May 05 '21 07:05 MetroMarv

Hi! what about using arrays??? I have this issue

I have to translate this

<label *ngIf="votingElection.userResults.some(r => r.votingOptionId === vOption.id)">Votes:</label>
<table class="table table-sm">
        <tr *ngFor="let vResult of votingElection.userResults">
            <td> {{ votingElection.userList.find(u => u.userId === vResult.UserId)?.name }} </td>
        </tr>
</table>

Into this:

<label *ngIf="anyResult(vOption.id)">Votes:</label>
<table class="table table-sm">
        <tr *ngFor="let vResult of votingElection.userResults">
            <td> {{ getUserName(vResult) }} </td>
        </tr>
</table>

I think this is absolutely necessary, don't you?!! :) cheers!

LeonardoX77 avatar Jul 30 '21 02:07 LeonardoX77

@LeonardoX77 Angular is not react. Don't write JS in html.

Lonli-Lokli avatar Jul 30 '21 07:07 Lonli-Lokli

@Lonli-Lokli How's that example is a React? It's not JSX.
It's just expressing yourself in declarative manner, why in a world do I need to create e.g. displayFn method on the component class each time? Or the method as simple as u => u.userId === vResult.UserId ?
I need to open the class each time and find the according methods, it's a waste of time and waste of readability.
I totally agree with you @LeonardoX77 , this is what I would expect from the framework that has the idea of "html templates".

haskelcurry avatar Jul 30 '21 07:07 haskelcurry

This is being tracked in the aggregate issue of #43485 for which we need to create a project proposal.

petebacondarwin avatar Oct 15 '21 15:10 petebacondarwin

Hi! what about using arrays??? I have this issue

I have to translate this

<label *ngIf="votingElection.userResults.some(r => r.votingOptionId === vOption.id)">Votes:</label>
<table class="table table-sm">
        <tr *ngFor="let vResult of votingElection.userResults">
            <td> {{ votingElection.userList.find(u => u.userId === vResult.UserId)?.name }} </td>
        </tr>
</table>

Into this:

<label *ngIf="anyResult(vOption.id)">Votes:</label>
<table class="table table-sm">
        <tr *ngFor="let vResult of votingElection.userResults">
            <td> {{ getUserName(vResult) }} </td>
        </tr>
</table>

I think this is absolutely necessary, don't you?!! :) cheers!

Nope. This code is wrong either way. Not only that it takes the first item it finds and ignores the rest (hidden try/catch, random behavior), but it also misuses the "?" operator (which is pure evil and accounts for thousands of bugs, seems like all the develoeprs use it wrong) The check should have been on the td element and not as late as possible. This operator should be cancelled.

jjamid avatar Aug 08 '23 12:08 jjamid

Well @jjamid, the ? operator used in my example is just to avoid errors but we can decide not to use it if we are sure the find() method will always return a value, that check can be made prior execution of my example, that's not the point of this thread and it's absolutely irrelevant. The real point of this is to allow support for arrow functions in html templates, which I'm not the only one who thinks it won't break html template philosophy and it will make definitely life easier. if my example is wrong or not, it doesn't matter, it's only illustrating the problem and it could be even illustrated with pseudo-code. Don't focus on what my code is doing and focus on what you could do instead if the arrow feature would be supported in html templates. Thanks Regards

LeonardoX77 avatar Aug 09 '23 13:08 LeonardoX77

I really hope it will never be implemented. It will lead to such an awful code which I usually see just in the JS portion, but it will also lead to performance degradation, which will be visible as a framework issue, while it will be an issue of a developer.

BestOfTheBestSir avatar Aug 09 '23 13:08 BestOfTheBestSir

Well @jjamid, the ? operator used in my example is just to avoid errors but we can decide not to use it if we are sure the find() method will always return a value, that check can be made prior execution of my example, that's not the point of this thread and it's absolutely irrelevant. The real point of this is to allow support for arrow functions in html templates, which I'm not the only one who thinks it won't break html template philosophy and it will make definitely life easier. if my example is wrong or not, it doesn't matter, it's only illustrating the problem and it could be even illustrated with pseudo-code. Don't focus on what my code is doing and focus on what you could do instead if the arrow feature would be supported in html templates. Thanks Regards

I don't agree. Commenting on problematic code is always relvant, even if it's not related, since it's public. You cannot "avoid errors"! You're just moving the problem to the next much harder to troubleshoot step. The "?" is responsible personally for thousands of bugs that I've seen in the developers' code (and it usually makes the code extremly hard to understand when used wrong), it's because it's the exact opposite of the Guard Clause idea.

Anyway, I think too that there should be arrow support in the angular html. And not only that, also access to Enums without workarounds and basically to be excatly like .NET's amazing razor files and Web Forms before that. Eventually the html will inherit the component, it's just a matter of time. They now allow accessing protected memebers in the html.

Everything can be abused (like the "?" operator) and it's not a reason to not implement it.

jjamid avatar Aug 09 '23 13:08 jjamid

Anyway, I think too that there should be arrow support in the angular html. And not only that, also access to Enums without workarounds (...)

welcome to the pros list and don't worry about my awful code :), probably you will never have to deal with it.

LeonardoX77 avatar Aug 09 '23 14:08 LeonardoX77

I'd like to make some arguments for this:

  • Angular templates are not currently valid HTML anyway. Structural directives, string interpolation, property and event bindings all violate HTML specs (and that's fine).
  • you can reduce mental overhead for developers by not having to rely on a mapping function in the typescript code (which should really be ignorant of these rendering details anyway).
  • you lower the learning curve for developers coming from other frameworks, particularly React.
  • you can deprecate ng-template and ngTemplateOutlet which are a bit obscure to most devs, and have unresolved issues around type-safety.
  • you could still impose restrictions on arrow functions that limit the potential abuse expressed in this thread, for example they should always return HTML, should not allow side-effects, and perhaps some other restrictions too.

Syntax proposal:

<my-select
  [renderOption]="data => (          <- Note that anything inside '(' and ')' can be parsed (as if its top-level HTML)
    @let name = data.name;           <- local variables are legal (as if its top-level HTML)
    @return <div>{{name}}</div>      <- new @return variable operator              
  )"
/>

StephenSPaul avatar May 18 '25 09:05 StephenSPaul