Trying to understand the basic flow between Cycle.js nested dialogs
A simple component which has three states (on) <-> (transitional state) <-> (off):
- You click on the component
- It propagates the click up to the parent
- The parent sets the component to a transitional state
- The parent retrieves data
- The parent sets the component to a final state
Imagine a checkbox in a CMS which triggers an action when you click it:
It's evident that such a component is generic enough to warrant being a separate component. However, it's not specialized enough to take care of data fetching or of state transitions. All it needs to do is to transfer clicks to the parent, get rendered with the appropriate state.
I will start with just an "on/off" element, just to see how it feels. It took me several tries to get this partially working, so here it is:
Create several "rows" of such checkboxes.
// main.js
/** @jsx hJSX */
import {hJSX} from '@cycle/dom';
import {run, Rx} from '@cycle/core';
import {makeDOMDriver} from '@cycle/dom';
import row from './table-row.js';
function main(sources){
const createRow = (i) => row(sources, i);
let arr = [];
for(let i = 0; i <= 5; i++){ arr.push(createRow(i).DOM); }
return {
DOM: Rx.Observable.just(<div>{arr}</div>)
};
}
run(main, {
DOM: makeDOMDriver('#app')
});
// table-row.js
/** @jsx hJSX */
import {hJSX} from '@cycle/dom';
import {Rx} from '@cycle/core';
import checkbox from './checkbox.js';
const id = (name) => `div-check-${name}`;
function model(checkBox, name){
let state = true;
return checkBox.state$.startWith(true).map(() => {
console.log(`props change ${name} from ${state} to ${!state}`);
state = !state;
console.log(`current state is ${state}`);
return {
state: state,
label: state === true ? 'Checked' : 'Unchecked'
}
});
}
function view(model$, checkboxRender, name){
const DOM$ = model$.map((props) => {
console.log(`DOM change in ${name} with `, props);
return checkboxRender(props)
});
return DOM$.map(
(DOM) => {
console.log('We got DOM', DOM);
return <div key={id(name)}>{DOM}</div>;
}
).map((DOM) => DOM);
}
export default function (sources, name){
const check = checkbox(sources, name);
const model$ = model(check, name);
const view$ = view(model$, check.render, name);
return {
DOM: view$
}
}
// checkbox.js
/** @jsx hJSX */
import {hJSX} from '@cycle/dom';
import {Rx} from '@cycle/core';
const id = (name) => `check-${name}`;
function intent(DOM, name){
return DOM.select(`#${id(name)}`).events('click').map((e) => {
e.preventDefault();
return true;
});
}
function view(name){
return ({state, label}) => {
console.log(`rendering ${name} with `, {state, label});
return <span>
<input type="checkbox" name={name}
id={id(name)}
className='.check'
key={id(name)}
checked={state}
/>
<label htmlFor={id(name)}>{`${label} (${state})`}</label>
</span>
};
}
export default function (sources, name){
const clickStream$ = intent(sources.DOM, name);
return {
render: view(name),
state$: clickStream$
};
}
- I need an extra
.map
on theDOM$.map
intable-row.js
view
function. Why?- Possibly because the result is an observable of observables
- (a vritual-dom question) Even though the
state
is passed correctly to therender
function incheckbox.js
, the attributechecked
is not propagated to the DOM- The property should be
defaultChecked
. (NOTE: there are still issues wit it no being properly rendered)
- The property should be
And, most importantly:
- Is this the way to do it?
- Answer: yes. You could also use a wrapper element on the checkbox and capture clicks to it in the parent.
Regarding the double
map
above. If I change the code slightly, I no longer need the second mapping. I'm baffled.