Runnable example: https://esnextb.in/?gist=978799bf48a7f914cbbd39df0213d672

I'm trying to create a multiselect component for cycle.js to see if I want to replace the current UI of an application with a cycle app.

I have one component for the options within the multiselect. Each of the options gets passed props in the form {selected: Bool, label: String, value: Int}

Here's the overview of the code:

The Option component listens for clicks on itself and toggles the 'selected' boolean by combining a stream of booleans generated by folding logical 'not' functions upon the initial boolean passed to the option in the props$ source.

The Option component then outputs a stream of it's state so that the Multiselect component can refer to it in order to return a stream of selected values and such.

The Multiselect component receives a props$ source which contains a stream with the options [{selected, label, value}...].

From these options it constructs a stream of arrays of Option components.

The stream of Option components is then flattened into a stream of arrays of virtual dom trees (one for each option).

The stream of Option components is also flattened into a stream of arrays of states (one for each option).

Here is where the problem begins:

Both of the above only log once if I call .observe(console.log) on them.

In order to display the multiselect I then map the stream of arrays of virtual dom trees of the options to generate the virtual dom tree of the multiselect.

I also combine this vtree stream with the state stream so that I can console.log both (since observe only logs them once).

When I log them, the vtree arrays do change, but the states of all options are always the same as their initial states.

This only happens if the options are isolated. If they aren't isolated, they all respond to clicks on any of them, but their states are logged as changed.

What is going on here? Why does isolate matter? Because all the combined streams don't fire every time? Is what I'm doing completely non-idiomatic?

http://pastebin.com/RNWvL4nf here's a pastebin as well. I wanted to put it on webpackbin, but it wouldn't download the packages I need.

import * as most from 'most';
import {run} from '@cycle/most-run';
import {div, input, p, makeDOMDriver, li, ul, span, h2} from '@cycle/dom';
import fp from 'lodash/fp'
import isolate from '@cycle/isolate'

// props : { selected : Bool, label : String, value: Int}
function Option({DOM, props$}) {
    const click$ = DOM.select('.option').events('click') 
    // a stream of toggle functions. one for each click
    const toggle$ = click$
                       .map(() => bool => !bool) 
    /// stream of toggle functions folded upon the inital value so that clicking the option checks if off and on  
    const selected$ = props$.map(prop => toggle$.scan((b, f) => f(b), prop.selected)).join()
    // a stream of states which has the same structure as props, but toggled 'selected' field according to selected$ stream 
    const state$ = most.combineArray((props, selected) => ({...props, selected}), [props$, selected$])
    // the state mapped to a representation of the option
    const vtree$ = state$.map(state => { 
        return li('.option', {class: {selected: state.selected}}, [
            input({attrs: {type: 'checkbox', checked: state.selected}}),
            p(state.label),
        ]) 
    })
    // returns the stream of state$ so that multiselect can output a stream of selected values
    return {
        DOM: vtree$,
        state$,
    }
}

function Multiselect({DOM, props$}) {
    // a stream of arrays of isolated Option components
    const options$ = props$.map(
        options => 
            options.map(it => 
                isolate(Option)({DOM, props$: most.of(it)}))) 
                // Option({DOM, props$: most.of(it)}))) // comment above line and uncomment this one. Without isolation the component doesn't work correctly, but the states are updated 

    // a stream of arrays of virtual tree representations of child Option components
    const optionsVtree$ = options$.map(options => fp.map(fp.get('DOM'), options))
                                  .map(arrayOfVtree$ => most.combineArray(Array, arrayOfVtree$))
                                  .join() 
    // a stream of arrays of states of child Option components 
    const optionStates$ = options$.map(options => fp.map(fp.get('state$'), options))
                                  .map(arrayOfState$ => most.combineArray(Array, arrayOfState$))
                                  .join() 
                                //   .map(fp.filter(fp.get('selected')))  

    // here the virtual trees of options are combined with the state stream so that I can log the state. I only use the virtual dom trees   
    const vtree$ = optionsVtree$.combine(Array, optionStates$).map(([vtrees, states]) => {
        console.log(states.map(state => state.selected)); // this always prints the initial states
        // console.log(vtrees); // but the vtrees do change
        return div('.multiselect', [
            ul('.options', vtrees)
        ])
    }) 

    const sinks = {
        DOM: vtree$,
        optionStates$
    };
    return sinks;
}

run(Multiselect, {
  DOM: makeDOMDriver('#app'),
  props$: () => most.of([
      {value: 0, label: 'Option 1', selected: false},
      {value: 1, label: 'Option 2', selected: false},
      {value: 2, label: 'Option 3', selected: false},
      {value: 3, label: 'Option 4', selected: false},
    ])
});

EDIT: franciscotln kindly made a working example which revealed something in mine. https://gist.github.com/franciscotln/e1d9b270ca1051fece868738d854d4e9 If the props are a simple array not wrapped within an observable it works. Maybe because if it's an observable of arrays it requires an observable of arrays of components instead of a simple array of child components.

However the two aren't equivalent. With my original version (where the options are a stream of arrays) the props$ can be something other than just an .of observable. It's value can change during the course of the program, whereas with the second I'm stuck with the original array.

Is there there some problem with a stream of arrays of isolated components?

const props = [{value: 0, label: 'Option 1', selected: false},
                 {value: 1, label: 'Option 2', selected: false},
                 {value: 2, label: 'Option 3', selected: false},
                 {value: 3, label: 'Option 4', selected: false}]
// a stream of arrays of isolated Option components
const options = props.map(prop =>
    isolate(Option)({
        DOM,
        props$: most.of(prop)
    }) 
);
1

There are 1 best solutions below

0
On

@tusharmath In the cycle gitter chat found out what I was doing wrong.

Apparently all I had to do was call .multicast() on the stream of arrays of option components:

const options$ = props$.map(
        options => 
            options.map(it => 
                isolate(Option)({DOM, props$: most.of(it)}))).multicast()

Something to do with hot and cold observables. Got burned by cold observables I suppose.

Here's his explanation.

Ive ill try to explain as much as I have understood from the experts here. isolate attaches a scope param to the virtual dom node that is created. now in your case you are creating the virtual dom twice

because of the two subscriptions here — DOM : https://esnextb.in/?gist=978799bf48a7f914cbbd39df0213d672 state$: https://esnextb.in/?gist=978799bf48a7f914cbbd39df0213d672 the two VDoms have different scopes effectively the event listeners are using a different scope and the dom element has a different scope.