gargantuan
gargantuan

Reputation: 8944

How do I create a dynamic, repeating list of elements with Cycle.js?

With Cycle.js, I'm trying to create a view that renders a dynamic number of components when given a set of data points. However, I can't figure out how to create repeating views.

I've stripped everything back to the most basic example of how I think it should work. Hopefully someone can point out what I'm missing.

/*
    Expected:
    Given an array of data objects, create the following DOM
    <div class="container">
        <h1 class=".data">Element 1</h1>
        <h1 class=".data">Element 2</h1>
        <h1 class=".data">Element 3</h1>
        <h1 class=".data">Element 4</h1>
        ...
    </div>

    Result:
    <div class="container">
        <h1 class=".data">Element 9</h1>
    </div>
*/

function view( data$ ){
    return Rx.Observable.of(
        div('.container', data$.map( data =>
            h1('.data', `Element: ${ data.id }`)
        ))
    );
}

function main( sources ) {

    // Create an array of objects
    const arr = [];
    for( let i = 0; i < 10; i++ ){
        arr.push({
            id: `id ${i}`
        })
    }

    // Convert array to an observable
    const data$ = Rx.Observable.from(arr);

    const vtree$ = view( data$ );

    return {
        DOM: vtree$
    };

}

const drivers = {
    DOM: CycleDOM.makeDOMDriver('#mountPoint')
};

Cycle.run( main, drivers );

Upvotes: 0

Views: 1128

Answers (2)

Brian C.
Brian C.

Reputation: 7986

Cycle has a Collection module. It takes a stream of arrays of objects and isolates them. I get these modules with npm and deploy with webpack.

import Cycle from '@cycle/xstream-run';
import Collection from '@cycle/collection';
import {div,h1} from '@cycle/dom';
import { makeDOMDriver } from '@cycle/dom';
import xs from 'xstream';

function view( comb$ ){
  return comb$.debug('view').map( tup =>{
    const item = tup[0];
    const clicked = tup[1];
    return div('.container', [ h1( '.data', `Element: ${item.id} clicked: ${clicked}`)]);
  });
}

/* Item Component */
function Item ( sources ){
  const intent$ = sources.DOM   //Events on DOM are limited to only ones on sink DOM
    .select('div')
    .events('click').mapTo('yes').startWith('no').debug('item clicks');
      
  return {
    DOM: view( xs.combine( sources.itemJaJa, intent$) )
  };
}

function main(sources) {

  const arr = [];
  for( let i = 0; i < 10; i++ ){
    arr.push(
      //The way Collection add the obj to the source is odd
      //this is what's worked for me
      { id_to_collection: i, //identifier used by collection
        itemJaJa:{ 
          id: `id ${i}`
        }
      } );
  }
  
  const items = Collection.gather(Item, sources, xs.of( arr),'id_to_collection'  );  
  const vtrees$ = Collection.pluck( items, item=>item.DOM);  
  return {
    DOM:  vtrees$.map( itemDomList => div('.top',itemDomList)),
  };    
}

const drivers ={
  DOM: makeDOMDriver('#app')
};

Cycle.run(main, drivers);

Upvotes: 0

Laszlo Korte
Laszlo Korte

Reputation: 1179

Your array has 10 items so the observable will emit 10 items. Observables represent data over time. So your observable represents 10 points in time. Of those only the most recent will be used. That's why you are only seeing the "element 9".

Instead of converting your array into an observable you might want to create an observable that contains only one item: you array.

Change Rx.Observable.from(arr) into Rx.Observable.just(arr)

The next issue is your view function:

function view( data$ ){
    return Rx.Observable.of(
        div('.container', data$.map( data =>
            h1('.data', `Element: ${ data.id }`)
        ))
    );
}

Your view function takes a data$ parameter. Read data$ as data stream or stream of data. So your function takes in a stream That's correct so far.

But now you are creating a new Observable via Observable.of. That's not what you want to do. Instead you might just want to to transform the data$ - ie the stream of your data - into a stream of virtual DOM nodes:

function view( data$ ){
    return data$.map( data =>
        // here you should return a virtual DOM node
    )
}

Remember: data$ is your data over time. For each point in time you have some data. And for each point in time you want to have some DOM tree.

function view( data$ ){
    return data$.map( data =>
        div('.container', 
          // data is not a single of your items but the whole array of items
          // hence we can map over it and transform each item into a child nod:
          data.map((item) => div('.item', item.id))
        )
    )
}

Watch out: data$.map is very different from data.map. The first transforms/maps an observable, the later transforms/maps an classical array.

Bonus:

for( let i = 0; i < 10; i++ ){
     arr.push({
        id: `id ${i}`
     })
 }

You are creating your array in a procedural way. You might prefer the functional way of creating an array of size 10 and transforming it into an array containing your items:

Array.apply(Array, {length: 10}).map(
    (_,index) => ({id: `id: ${index}`})
)

Upvotes: 6

Related Questions