angular
angular copied to clipboard
Make events/streams more idiomatic in AngularDart
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?
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.
@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.
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 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.
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.
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.
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) { … }
}
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
- And an identity transform, i.e.
@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
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.