如何使用 RxJS 在 Cyclejs 中将 Observable 流转换为单独的可更新 div

How to convert a Observable stream into individual updatable divs in Cyclejs with RxJS

我正在尝试获取一个包含 10 个对象的随机 Observable 流,每个对象都具有如下属性:

{tileNum: '6', tileName: 'game-of-thrones' clickCount:'1'}

接下来,每当对象内部的相同 tileNum 使用 tileNum 通过流时,clickCount 就需要增加。现在对于棘手的一点,流式传输的每个 tileNum 需要附加到 div (0-9),这些 div 需要预先填充并且 clickCount增加附加到对象的相关 div。 例如:

div 6 = clickCount:'1' => {tileNum: '6'..} => div 6 = clickCount:'2'

到目前为止我有:

const tile = (sources) => {
  const incomingMessages$ = sources.socketIO.get('newClickStream'); // continuous Observable
  const state$ = model(incomingMessages$);
  const view$ = view(state$);
  return {
    DOM: view$,
  }
};

index.js

const model = actions =>
  actions.groupBy((tile) => tile.tileNum)
    .flatMap(
      (tile$) =>
        tile$
          .scan((prev, {tileName, tileNum}, index) => ({
          tileNum,
          tileName,
          clickCount: index + 1
        }))
          .do(x => console.log(x))
    );
export default model;

model.js

const view = (state$) =>
  state$.map(source => {
    return div([
      h(`div#${source.tileNum}`, {}, ['${source.clickCount}']),
    ])
  });
export default view;

view.js

现在我只能填充一个 div,它会在每个新流对象上发生变化。作为警告,但如果 div 可以由 clickCount 订购,那将是惊人的。

我稍微简化了代码并插入了解释作为注释。

const Cycle = require('@cycle/core');
const {makeDOMDriver, div} = require('@cycle/dom');
const {Observable} = require('rx');

const model = (tiles, actions) =>
  // since we already know all the tiles we just a a stream with one item which represents all the current tiles.
  Observable.just(
    // we map the array of tiles into an array of streams
    tiles.map(
    (t) => {
      return actions.incomingMessages$
        // As each of those streams belongs to one specific tile we filter
        // the click events to match this tile
        .filter((click) => {
          return click.tileNum === t
        })
        // We start with a click count of 0
        .startWith({
          tileNum: t,
          count: 0
        })
        // for each click we increse the click count by 1
        .scan(({tileNum, count}) => ({
          tileNum,
          count: count+1,
        }))
    }
  ))
;

const view = (state$) =>
  // The view is a mapping from the current stream to a vnode tree
  // the state contains the list of all tiles as array
  // This is a stream mapping
  state$.map((tiles) => div(
    // the list of tiles is a plain array
    // we map each tile into a vnode
    // this is a plain array mapping
    tiles.map((tile) => 
      // Each tile itself is a stream of click counts
      // We are only interessted in the latest click count
      // We map this stream into vnodes
      tile.map((t) => 
        // t is the the object containing the tiles id and the current click count
        // We just create a vnode populated with this data.
        div('.tile', {
          attributes: {'data-tile-num': t.tileNum}
        },t.tileNum+': '+t.count))
    )
  )
);

const main = (sources) => {
  // I replaced your incoming messages with a stream I get from click events.
  const incomingMessages$ = sources.DOM
    .select('.tile').events('click')
    .map((evt) => ({tileNum: parseInt(evt.target.dataset.tileNum, 10)}));

  // You already know all the tiles which exist.
  // You do not only want to show clicked tiles but even tiles which have never been clicked.
  // That's why instead of collection the tiles itself from a stream via groubBy
  // which can just create an array with all the tiles.
  const tiles = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,77,18,19];
  const state$ = model(tiles, {incomingMessages$});
  const view$ = view(state$);
  return {
    DOM: view$,
  }
};

const sources = {
  DOM: makeDOMDriver('.app')
}

Cycle.run(main, sources);

You can paste the code here to run it in your browser