Skip to content

Proposal: Support declarative binding from View events to Observables #4062

Closed

Description

@robwormald

Not sure this is a good idea, or even possible, but thought I'd throw it out there.

In vanilla Rx, if I wanted to listen to a DOM event to do a typeahead or something I'd do something like:

//get DOM element
const input = document.getElementById('myInput');
//observable of click events
const inputs$ = Rx.Observable.fromEvent(input, 'keyup');
//map changes to requests
const responses$ = inputs$.flatMap(ev => myHttpService.get(ev.target.value)); 
//subscribe to responses
responses$.subscribe(json => console.log(json));

In current ng2, if I wanted to do the equivalent, I'd do something like:

@Component({
  selector: 'my-component'
})
@View({
  template: `
     <div [ng-form-model]="myForm">
       <input type="text" ng-control="query">
    </div>
    <ul>
     <li *ng-for="#result of results | async">{{result.text}}</li>
   </ul>
`
})
class MyComponent {
  constructor(public http: Http, fb: FormBuilder){
    //create form group
    this.myForm = fb.group({
      query: ""
    });
    //subscribe to value changes, map to responses, bind to view w/ pipe
    this.results = this.myForm.controls.query.valueChanges
      .toRx()
      .flatMap(query => this.http.get(`/foos?q=${query}`)
      .map(res => res.json());
  }
}

This all works great (or will, once Rx stuff settles).

However, if I wanted to do the same sort of thing from an arbitrary event (unrelated to forms, like a mousedrag or button click or whatever), there's not a good way (I can see) to get an Observable from a DOM event, so you have to do something like:

<button (click)="makeRequest($event)">Make Request</button>
//snip
class MyComponent {
  makeRequest($event){
    this.http.get($event.target.url)
      .map(res => res.json())
      .subscribe(results => this.results = results);
  }
}

which isn't nearly as clean.

Proposed idea would be to provide a way to delegate / bind an arbitrary DOM event to a Subject/Observable, so that it could be easily subscribed to and handled reactively.

Something like (totally made up syntax here, point being we're binding the click event to the Subject):

Edit: see #4062 (comment) for current proposal

<button [(click)]="clicks$">make request</button>
class MyComponent {
  clicks$ = new Rx.Subject();
  constructor(){
    this.results = this.clicks$
      .flatMap(ev => http.get('someurl.json'))
      .map(res => res.json())
  }
}

I saw #3992, but I believe this is for emitting events "out" from the component, whereas this idea is more for usage within a component.

Thoughts?

Activity

  1. PatrickJS commented on Sep 9, 2015

    @PatrickJS
    Contributor

    👍 it would be awesome to have Rx Subjects for two-way data-binding

    github-tipe-logo

  2. dsebastien commented on Sep 9, 2015

    @dsebastien

    +1, let's RX all the things :)

  3. jeffbcross commented on Oct 29, 2015

    @jeffbcross
    Contributor

    I was chatting with @vsavkin earlier today about this, but more from the perspective of putting the component in control instead of the template. He had the idea of extending the @ViewChild property decorator to something like @ObserveChildOrBetterName('.some-button', 'click'),

    class MyComponent implements AfterViewInit {
      @ObserveChild('.search-button', 'click') searchClick:EventEmitter<MouseEvent>;
    
      afterViewInit () {
        this.searchClick.subscribe(evt => alert('clicked'));
      }
    }

    This could work for native and custom events, and, aside from setting the MouseEvent as the generic type, has no tight coupling to the DOM, so this component is more portable to different platforms.

  4. ericmartinezr commented on Oct 30, 2015

    @ericmartinezr
    Contributor

    @jeffbcross that would be beautiful 👍. Although I would call it @ObserveOutput() to keep it aligned with the latest naming.

  5. flyingmutant commented on Nov 16, 2015

    @flyingmutant

    It would be nice to also use the hypothetical @ObserveChild() to translate this:

    @ng.Component({
      template: `<input [ng-form-control]="inputControl">`
    })
    class Something {
      inputControl = new ng.Control();
      constructor() {
        this.inputControl.valueChanges.subscribe(...)
      }
    }

    into

    @ng.Component({
      template: `<input #my-input>`
    })
    class Something {
      @ng.ObserveChild('myInput', 'input') inputChanges: EventEmitter<Event>;
      constructor() {
        this.inputChanges.map(ev => ev.target.value).subscribe(...)
      }
    }

    Having to use [ng-form-control] to observe the values does not feel very natural currently, and clutters the template as well.

  6. flyingmutant commented on Nov 16, 2015

    @flyingmutant

    Aside: isn't it amusing how the things move from onclick-style (attach things right in the DOM), to jquery-style (attach things after the fact using selectors), to angular1-style (= onclick-style reborn), to angular2-style (@ViewChild, @ObserveChild etc. – jquery-style reborn)?

    #CleanTemplates2015

  7. 0x-r4bbit commented on Nov 29, 2015

    @0x-r4bbit
    Contributor

    Love @jeffbcross and @vsavkin proposal. Thanks @robwormald for bringing this up.

    @ericmartinezr as a site note, I think @ObserveOutput doesn't really fit here, as "outputs" describe what goes out of the component. This pattern however, describes how to listen/subscribe to something inside the component as part of the component's view. So it's not really an output.

    But i'm sure we're going to find a nice name here, as naming is sooo easy.

  8. danculley commented on Dec 22, 2015

    @danculley

    Not to add to the naming soup, but I think @ObserveChildEvent() and @ObserveHostEvent() would make the most sense.

    @ObserveChild() is not sufficiently generic, as the same concept makes sense for observing DOM events on the host element as well. The semantics also do not match exactly, because the bound variable is observing an event on the child, not the child itself.

    @ObserveOutput() is (as @PascalPrecht describes above) confusing because it is not just an observable version of @Output().

    @ObserveEvent() identifies the event, but not where it is coming from.

  9. thelgevold commented on Dec 31, 2015

    @thelgevold
    Contributor

    I have been playing with Observables tied to a UI element and some event.

    One issue I've had is passing data via the event - without storing and passing state from the DOM.
    I have been using fromEvent, but I end up having to pull info from the DOM element to make it work. The use case is to pass an id to build a dynamic url when an element is clicked.

    I would love to be able to just pass arbitrary model data to processes triggered by the event. I would also like to abstract the underlying element and perhaps group it with other elements with the same behavior, but with different data.

    One idea I came up with was a concept of an Observable group

    <div [ObservableGroup]=['group1',{country:'usa'}]>USA</div>
    <div [ObservableGroup]=['group1',{country:'germany'}]>Germany</div>
    
    //Made up function
    Observable.fromGroupAndEvent('group1','click')
    .switchMap((r:any) => 
        {  
            this.activeCountry = r.data.country;
            return this.http.get('./country-info/' + this.activeCountry + '.json')
        })
    .map((res: Response) => res.json())
    .subscribe(r => this.capitol = r.capitol);
    

    I am new to Observables, so I am not sure this is the right direction, but it does seem to meet some of my goals

  10. edispring commented on Jan 18, 2016

    @edispring

    👍 for @ng.ObserveChild()

  11. valorkin commented on Jan 19, 2016

    @valorkin
    Contributor

    Observe all the things! 👍

  12. mhevery commented on Jan 27, 2016

    @mhevery
    Contributor

    I think this is a good idea, post RC.

  13. 46 remaining items

  14. mgechev commented on Jul 14, 2016

    @mgechev
    Member

    I like the proposal. The declarative bindings will not only enforce a better style of reactive programming but also stimulate applications with logic-less templates, which will increase cross-platform portability.

    On top of that, having access to the Subjects in the controllers' constructors will help us write even more declarative code (as the prototype of @gdi2290 shows).

  15. added
    area: coreIssues related to the framework runtime
    and removed on Sep 7, 2016
  16. fxck commented on Oct 6, 2016

    @fxck

    Is this being worked on now that final is out?

  17. PatrickJS commented on Oct 10, 2016

    @PatrickJS
    Contributor

    I have a lib published if someone wants to experiment with the decorators

    github-tipe-logo

  18. fxck commented on Oct 10, 2016

    @fxck

    Sure, where can I find it?

  19. otodockal commented on Oct 10, 2016

    @otodockal
    Contributor
  20. amcdnl commented on Nov 8, 2016

    @amcdnl
    Contributor

    @robwormald @IgorMinar - Is there any status updates on this?

    I spoke w/ @gdi2290 the other day and we think I've got a pretty good use case for the issue being discussed.

    In angular2-data-table I have the ability for users to re-order table header columns using 2 controls: orderable and draggable. In this scenario, draggable is completely decoupled from orderable. The header container is the orderable and the column cells are draggables. Code.

    img

    The orderable uses ViewChildren to subscribe to 2 events on each of the draggables like:

    @ContentChildren(DraggableDirective, { descendants: true })
    private draggables: QueryList<DraggableDirective>;
    

    unfortunately since the column headers that are draggables can change, I have to use Differs to constantly diff the columns, unsubscribe from old one and subscribe to new ones like so:

      @ContentChildren(DraggableDirective, { descendants: true })
      private draggables: QueryList<DraggableDirective>;
    
      private differ: any;
    
      constructor(differs: KeyValueDiffers) {
        this.differ = differs.find({}).create(null);
      }
    
      ngAfterContentInit() {
        this.updateSubscriptions();
        this.draggables.changes.subscribe(
            this.updateSubscriptions.bind(this));
      }
    
      ngOnDestroy() {
        this.draggables.forEach(d => {
          d.dragStart.unsubscribe();
          d.dragEnd.unsubscribe();
        });
      }
    
      updateSubscriptions() {
        const diffs = this.differ.diff(this.draggables.toArray());
    
        if(diffs) {
          const subscribe = ({ currentValue, previousValue }) => {
            unsubscribe({ previousValue });
    
            if(currentValue) {
              currentValue.dragStart.subscribe(this.onDragStart.bind(this));
              currentValue.dragEnd.subscribe(this.onDragEnd.bind(this));
            }
          };
    
          const unsubscribe = ({ previousValue }) => {
            if(previousValue) {
              previousValue.dragStart.unsubscribe();
              previousValue.dragEnd.unsubscribe();
            }
          };
    
          diffs.forEachAddedItem(subscribe.bind(this));
          diffs.forEachChangedItem(subscribe.bind(this));
          diffs.forEachRemovedItem(unsubscribe.bind(this));
        }
      }
    

    using @gdi2290 approach this could be very clean.

  21. PatrickJS commented on Dec 17, 2016

    @PatrickJS
    Contributor

    see current issue for better rxjs support
    #13248
    github-tipe-logo

  22. amcdnl commented on Dec 19, 2016

    @amcdnl
    Contributor

    @gdi2290 - should this be closed now?

  23. mhevery commented on Dec 20, 2016

    @mhevery
    Contributor

    Closed in favor of #13248

  24. angular-automatic-lock-bot commented on Sep 13, 2019

    @angular-automatic-lock-bot

    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.

  25. locked and limited conversation to collaborators on Sep 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: coreIssues related to the framework runtimeeffort2: daysfeatureLabel used to distinguish feature request from other issuesstate: Needs Design

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions