I may be attempting something foolish, but I have a sufficiently-large non-onionified Cycle.js app and I’m trying to learn how onionify works, so I’d like to embed an onionified component into my original non-onion app.
So I have a simple onion-ready component, the increment/decrement example, and I have a simple non-onion Cycle app, the “Hello Last Name” example—how do I smoosh the two together so I have the incrementer component and the Hello component one after the other in the same webpage?
Counter.ts
, onion-ready component
import xs from 'xstream';
import run from '@cycle/run';
import { div, button, p, makeDOMDriver } from '@cycle/dom';
export default function Counter(sources) {
const action$ = xs.merge(
sources.DOM.select('.decrement').events('click').map(ev => -1),
sources.DOM.select('.increment').events('click').map(ev => +1)
);
const state$ = sources.onion.state$;
const vdom$ = state$.map(state =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + state.count)
])
);
const initReducer$ = xs.of(function initReducer() {
return { count: 0 };
});
const updateReducer$ = action$.map(num => function updateReducer(prevState) {
return { count: prevState.count + num };
});
const reducer$ = xs.merge(initReducer$, updateReducer$);
return {
DOM: vdom$,
onion: reducer$,
};
}
index.ts
, non-onion main app
import xs, { Stream } from 'xstream';
import { run } from '@cycle/run';
import { div, input, h2, button, p, makeDOMDriver, VNode, DOMSource } from '@cycle/dom';
import Counter from "./Counter";
import onionify from 'cycle-onionify';
const counts = onionify(Counter);
interface Sources {
DOM: DOMSource;
}
interface Sinks {
DOM: Stream<VNode>;
}
function main(sources: Sources): Sinks {
const firstName$ = sources.DOM
.select('.first')
.events('input')
.map(ev => (ev.target as HTMLInputElement).value)
.startWith('');
const lastName$ = sources.DOM
.select('.last')
.events('input')
.map(ev => (ev.target as HTMLInputElement).value)
.map(ln => ln.toUpperCase())
.startWith('');
const rawFullName$ = xs.combine(firstName$, lastName$)
.remember();
const validName$ = rawFullName$
.filter(([fn, ln]) => fn.length > 0 && ln.length >= 3)
.map(([fn, ln]) => `${ln.toUpperCase()}, ${fn}`);
const invalidName$ = rawFullName$
.filter(([fn, ln]) => fn.length === 0 || ln.length < 3)
.mapTo('');
const name$ = xs.merge(validName$, invalidName$);
const vdom$ = name$.map(name =>
div([
p([
'First name',
input('.first', { attrs: { type: 'text' } }),
]),
p([
'Last name',
input('.last', { attrs: { type: 'text' } }),
]),
h2('Hello ' + name),
]),
);
return {
DOM: vdom$,
};
}
run(main, {
DOM: makeDOMDriver('#main-container'),
});
Attempts
If I replace run(main, ...)
with run(counts, ...)
, as the cycle-onionify docs advise for a fully-onionified app, I see only the counter as expected.
But counts
, as the output of onionify(Counter)
, is a function, so I don’t think I can “instantiate” it inside my `main().
Similarly, I don’t think I can create a counter component by calling Counter()
inside main because that function requires a sources.onion
input, and I’m not sure how to create that .onion
field, which has type StateSource
.
Question
How exactly can I use this Counter
onion-ready component inside my non-onionified main
?
Full example
Full example is available at https://gist.github.com/fasiha/939ddc22d5af32bd5a00f7d9946ceb39 — clone this, npm install
to get the requisite packages, then make
(this runs tsc
and browserify
to convert TypeScript→JavaScript→browserified JS).