angular icon indicating copy to clipboard operation
angular copied to clipboard

Make events/streams more idiomatic in AngularDart

Open natebosch opened this issue 6 years ago • 9 comments

This seems to come up a lot.

Problem: I want to "wrap" a component and add some new behavior or change the way it looks, but I want to forward through events from that component, possibly with some additional filtering or mapping on events.

Todays solution: Add a StreamController field, an @Output Stream getter, and a callback for the events.

This ends up not feeling very Darty because I don't normally need to deal with StreamController when I want to filter or map a Stream. Could we explore other syntax possibilities for this use case?

natebosch avatar May 08 '18 00:05 natebosch

This happens a lot in our code base as well. We are using @ViewChild when possible.

class WrapperComponent {
  @ViewChild(InnerComponent)
  InnerComponent comp;

  Stream<bool> get onEvent => comp.onEvent.map((t) => t != 'test');
}

How about providing a @DelegateOutput which uses a selector like @ViewChild. But I have no idea how to support further transformations without https://github.com/dart-lang/sdk/issues/4596.

class WrapperComponent {
  @DelegateOutput('#inner(click)', (Stream<String> stream) => stream.map((t) => t != 'test')
  Stream<bool> onEvent;
}

The second argument of @DelegateOutput could be optional. Also this doesn't feel Darty enough.

eredo avatar May 08 '18 03:05 eredo

@leonsenft and I had a discussion about this last week, and we had several good reasons for not doing this. I wish I wrote them down :). Happy to leave this issue open for discussion/PROs/CONs for now.

matanlurey avatar May 08 '18 17:05 matanlurey

One idea that has come up in discussions is the ability to define Outputs as a transformation only, via a callback method for an event on an inner component. Then, Angular would handle the wiring for you.

Example:

@Component(
  template: '<inner-component (click)="onClick($event.target)"></inner-component>'
class WrapperComponent {
  @Output('event')
  bool onClick(String clicked) => clicked == 'true';
}

alorenzen avatar May 14 '18 20:05 alorenzen

@alorenzen I like that idea. What about propagating the stream as-is though with no transformation? Obviously we could bind an identity function,

@Output('click')
Event onClick(Event event) => event;

but it seems silly to introduce any overhead for this. The first thought that comes to mind is simply check if the bound method returns the argument, but I don't think we could do that today since we don't analyze method bodies.

leonsenft avatar May 14 '18 21:05 leonsenft

it seems silly to introduce any overhead for this

FWIW I don't mind this overhead. It makes your intention explicit - "I am exposing these events unchanged" and it still feels Darty in that I'm not dealing with StreamController just to forward things.

natebosch avatar May 14 '18 22:05 natebosch

I agree with @natebosch on this one. Also, an engineer on a internal team who originally proposed this to me said that he looked through the codebase and found many fewer instances of the "straight, as-is pass through" than he expected to find.

alorenzen avatar May 14 '18 23:05 alorenzen

That was me. I also think that in many cases using @Output is actually overkill. If you know you'll never have have multiple listeners (for example in an app-specific component used as part of a bigger component), it's quite enough to just have an @Input that is a callback. I realize this goes against the standard though.

Example:

typedef FutureOr<Null> SaveAction(int foo, int bar);

@Component(
    template = '<button (click)="onSave?.call(foo, bar)">Save</button>')
class ChildComp {
  @Input()
  SaveAction onSave;
}

@Component(
    template = '<child-comp [onSave]="save"></child-comp>')
class ParentComp {
  void save(int foo, int bar) { … }
}

parren-google avatar May 15 '18 15:05 parren-google

I personally like the @alorenzen's suggestion:

  • It's explicit (you have to declare you want an @Output() bubbled up)
  • It gives a clear, unambiguous syntax to apply transformations, if desired
    • And an identity transform, i.e. @Output() Foo void onFoo(Foo foo) => foo is easy enough

@leonsenft:

but it seems silly to introduce any overhead for this.

I wouldn't be surprised if the identity function is compiled-out in Dart2JS. I could be wrong.

@parren-google:

@Input() is a callback seems simple enough, but has a lot of other edge cases:

  • We'd have to change detect functions, something we don't do today
  • It's sort of overloading the concept of Output and Input
  • It would require a lot more work

matanlurey avatar Jun 27 '18 00:06 matanlurey

Also lets not conflate creating delegating components with bubbling events (for example this would likely include inputs, not just outputs). We should track the request for delegates elsewhere if possible.

matanlurey avatar Jun 27 '18 00:06 matanlurey