使用 observables 跟踪 typeahead 列表中的当前位置

Track current position in typeahead list with observables

我正在使用 TypeScript 自动完成组件(Angular 2,尽管这不是问题的核心),我想通过 RxJs 中的可观察对象来管理大部分(或全部)它( 5.0.0-beta.8)。通过 up/down 箭头键操作时,我在跟踪建议列表中的当前项目位置时遇到问题:索引仍然停留在 0。

以下所有逻辑都放在一个单独的 class 中,它接收输入 observables 并产生要订阅的输出 observables(这就是为什么与 Angular 2 不严格相关)。客户端代码正确订阅输出可观察对象。

这是一些代码:

// Component responsible for managing only the list of suggestions
// It receives inputs from text field and it produces outputs 
// as current index in list, when to hide list, etc.
class AutocompleteListDriver {
  currentIndex$: Observable<number>;
  doClose$: Observable<void>;
  // ...

  constructor(...
    matches$: Observable<string[]>, // list of suggestions matching text in field
    keyUp$: Observable<KeyboardEvent>, // keyup events from text field
    keyDown$: Observable<KeyboardEvent>, // keydown events from text field
    ...) {

    const safeMatches$ = matches$
      .startWith([]);  // start with a clear, known state internally

    // when list is empty, component is hidden at rest:
    // detect keys only when component is visible
    const isActive$ = safeMatches$
      .map(matches => matches.length !== 0);

    const activeKeyUp$ = keyUp$
      .withLatestFrom(isActive$)
      .filter(tuple => tuple[1]) // -> isActive
      .map(tuple => tuple[0]);   // -> keyboardEvent

    this.currentIndex$ = safeMatches$
      .switchMap(matches => {
        const length = matches.length;
        console.log('length: ' + length);

        const initialIndex = 0;

        const arrowUpIndexChange$ = activeKeyUp$
          .filter(isArrowUpKey)
          .map(_ => -1);

        const arrowDownIndexChange$ = activeKeyUp$
          .filter(isArrowDownKey)
          .map(_ => +1);

        const arrowKeyIndexChange$ = Observable
          .merge(arrowUpIndexChange$, arrowDownIndexChange$)
          .do(value => console.log('arrow change: ' + value));

        const arrowKeyIndex$ = arrowKeyIndexChange$
          .scan((acc, change) => {
            // always bound result between 0 and length - 1
            const index = limitPositive(acc + change, length);

            return index;

          }, initialIndex)
          .do(value => console.log('arrow key index: ' + value))
          .startWith(0);

        return arrowKeyIndex$;
      })
      .do(value => console.log('index: ' + value))
      .share();
  }
}

想法是,每次发出新的匹配项(建议)列表时,列表中的当前索引应该开始一个新的 "sequence",可以这么说。这些序列中的每一个都从 0 开始,由于箭头 down/up 键而侦听 increments/decrements,通过注意不要超出 lower/upper 限制来累积它们。

开始一个新序列 对我来说它翻译成 switchMap。但是使用这样的代码,控制台只显示:

length: 5
index: 0

和箭头 up/down 键根本没有被检测到(尝试在 arrowDownIndexChange$ 上插入其他日志),所以没有更多的日志并且对最终组件没有影响。就像他们的可观察对象不再 订阅 一样,但据我所知 switchMap 应该订阅最新生成的序列和所有以前的 drop/unsubscribe。

只是为了尝试,我改用了 mergeMap:在这种情况下会检测到箭头键,但当然问题是所有序列(由于之前设置的匹配项)都合并在一起并且它们值相互重叠。除了这是不正确的,有时匹配列表会变空,所以总有一个点当前索引序列总是保持在 0。这个序列与所有其他序列合并并重叠,给出索引卡在的最终结果0.

我做错了什么?

好的。显然我之前通过 rxjs 文档发现的错误(当然我在 写下问题后得到这个,然后寻找更多见解)。

看来 switchMap 不是这个案例的正确工具。我应该使用 switch() 代替。这应该不足为奇,来自 .Net Reactive Extensions 背景......但我不知道为什么语言之间的命名变化经常让我误会。

工作解决方案应该是:

this.currentIndex$ = safeMatches$
  // use map to generate an "observable of observables"
  // (so called higher-order observable)
  .map(matches => {
    const length = matches.length;
    // ...
  })
  // "unwrap" higher order observable, by always 
  // keeping subscribed to the latest inner observable
  .switch()
  .do(value => console.log('index: ' + value))

编辑
这还不够。我还不得不放弃 activeKeyUp$ 以支持原始 keyUp$

如果有人有其他建议(或对当前行为的解释,例如activeKeyUp/keyUp),请随时回答。

编辑 #2
感谢@paulpdaniels 评论:事实证明,唯一真正的问题是 activeKeyUp$keyUp$ 错误。将 map().switch() 替换为 switchMap() 不会以任何方式损害功能。