如何从Angular2组件模板子元素事件创建RxJS可观察流

gwh*_*whn 13 observable rxjs typescript angular

我使用Angular 2.0.0-rc.4和RxJS 5.0.0-beta.6.

我正在尝试从事件中创建可观察流的不同方法,但是被选项所淹没,并且想要征求意见.我很欣赏没有一个适合所有人的解决方案,并且有适合课程的马匹.可能还有其他我不了解或未考虑的技术.

角2组件交互菜谱提供了一个父组件与子组件的事件进行交互的几种方法.但是,只有父和子通过服务进行通信的示例使用了可观察对象,对于大多数情况来说似乎有点过分.

场景是模板的元素发出大量事件,我想定期知道最新值是什么.

我使用1000ms ObservablesampleTime方法来监视<p>HTML元素上的鼠标位置.

1)此技术使用ElementRef注入组件的构造函数来访问nativeElement属性并通过标记名称查询子元素.

@Component({
  selector: 'watch-child-events',
  template: `
    <p>Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];

  constructor(private el:ElementRef) {}

  ngOnInit() {
    let p = this.el.nativeElement.getElementsByTagName('p')[0];
    Observable
      .fromEvent(p, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}
Run Code Online (Sandbox Code Playgroud)

共识意见似乎对这种技术不屑一顾,因为Angular2提供了对DOM的充分抽象,因此很少(如果有的话)需要直接与它进行交互.但是这种fromEvent工厂方法Observable使得使用起来很诱人,这是第一种想到的技术.

2)这种技术使用的EventEmitter是一种Observable.

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  emitter:EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();

  ngOnInit() {
    this.emitter
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.emitter.emit(e);
  }
}
Run Code Online (Sandbox Code Playgroud)

此解决方案避免查询DOM,但事件发射器用于从子级到父级进行通信,并且此事件不用于输出.

在这里读到,不应该假设事件发射器在最终版本中是可观察的,因此这可能不是一个可靠的特性可依赖.

3)该技术使用可观察的Subject.

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}
Run Code Online (Sandbox Code Playgroud)

在我看来,这个解决方案在所有框中都没有增加太多的复杂性.我ReplaySubject订阅它时可以使用a 来接收已发布值的完整历史记录,或者只使用最新版本的历史记录(如果存在)subject = new ReplaySubject<MouseEvent>(1);.

4)该技术将模板引用与@ViewChild装饰器结合使用.

@Component({
  selector: 'watch-child-events',
  template: `
    <p #p">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild('p') p:ElementRef;

  ngAfterViewInit() {
    Observable
      .fromEvent(this.p.nativeElement, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}
Run Code Online (Sandbox Code Playgroud)

虽然它有效,但它对我来说有点味道.模板引用主要用于模板中的组件交互.它还通过nativeElement使用字符串触及DOM 来引用事件名称和模板引用,并使用AfterViewInit生命周期钩子.

5)我扩展了示例以使用自定义组件来Subject定期管理和发出事件.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();
  subject = new Subject<MouseEvent>();

  constructor() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.event.emit(e);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}
Run Code Online (Sandbox Code Playgroud)

它由父母使用,如下所示:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];

  handle(e:MouseEvent) {
    this.messages.push(`${e.type} (${e.x}, ${e.y})`);
  }
}
Run Code Online (Sandbox Code Playgroud)

我喜欢这种技巧; 自定义组件封装了所需的行为,并使父进程易于使用,但它只传达组件树,不能通知兄弟.

6)对比这种技术简单地将事件从孩子转移到父母.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();

  handle(e:MouseEvent) {
    this.event.emit(e);
  }
}
Run Code Online (Sandbox Code Playgroud)

并使用@ViewChild装饰器连接到父级,如下所示:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild(ChildEventProducerComponent) child:ChildEventProducerComponent;

  ngAfterViewInit() {
    Observable
      .from(this.child.event)
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}
Run Code Online (Sandbox Code Playgroud)

7)或者像这样:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}
Run Code Online (Sandbox Code Playgroud)

使用可观察的Subject,与早期技术相同.

8)最后,如果您需要在组件树中广播​​通知,那么共享服务似乎是可行的方法.

@Injectable()
export class LocationService {
  private source = new ReplaySubject<{x:number;y:number;}>(1);

  stream:Observable<{x:number;y:number;}> = this.source
    .asObservable()
    .sampleTime(1000);

  moveTo(location:{x:number;y:number;}) {
    this.source.next(location);
  }
}
Run Code Online (Sandbox Code Playgroud)

该行为封装在服务中.子组件中所需的全部内容是LocationService在构造函数中注入并moveTo在事件处理程序中调用.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  constructor(private svc:LocationService) {}

  handle(e:MouseEvent) {
    this.svc.moveTo({x: e.x, y: e.y});
  }
}
Run Code Online (Sandbox Code Playgroud)

在需要广播的组件树级别注入服务.

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent],
  providers: [LocationService]
})
export class WatchChildEventsComponent implements OnInit, OnDestroy {
  messages:string[] = [];
  subscription:Subscription;

  constructor(private svc:LocationService) {}

  ngOnInit() {
    this.subscription = this.svc.stream
      .subscribe((e:{x:number;y:number;}) => {
        this.messages.push(`(${e.x}, ${e.y})`);
      });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
Run Code Online (Sandbox Code Playgroud)

完成后不要忘记取消订阅.该解决方案以牺牲一些复杂性为代价提供了很大的灵活性.

总之,如果不需要组件间通信,我会在组件内部使用Subject(3).如果我需要传达组件树,我会在子组件中封装一个Subject,并在组件(5)中应用流操作符.否则,如果我需要最大的灵活性,我会使用服务来包装流(8).

Max*_*kin 1

在方法 6 中,您可以使用事件绑定(向下滚动到“使用 EventEmitter 自定义事件”)而不是@ViewChildngAfterViewInit,这简化了很多:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="onEvent($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];
  onEvent(e) { this.messages.push(`${e.type} (${e.x}, ${e.y})`); }
}
Run Code Online (Sandbox Code Playgroud)