Skip to content

Commit 4c294b9

Browse files
committed
makeComponent supports sending events up through props handlers
1 parent 0c5d26d commit 4c294b9

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

readme.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ const CycleApp = makeComponent(main);
210210

211211
Besides `makeComponent`, this library also provides the `makeCycleReactComponent(run)` API which is more powerful and can support more use cases.
212212

213-
It takes one argument, a `run` function which should set up and execute your application, and return three things: source, sink, (optionally:) dispose function.
213+
It takes one argument, a `run` function which should set up and execute your application, and return three things: source, sink, (optionally:) events object, and dispose function.
214214

215-
- `run: () => {source, sink, dispose}`
215+
- `run: () => {source, sink, events, dispose}`
216216

217217
As an example usage:
218218

@@ -222,11 +222,22 @@ const CycleApp = makeCycleReactComponent(() => {
222222
const program = setup(main, {...drivers, react: reactDriver});
223223
const source = program.sources.react;
224224
const sink = program.sinks.react;
225+
const events = {...program.sinks};
226+
delete events.react;
227+
for (let name in events) if (name in drivers) delete events[name];
225228
const dispose = program.run();
226-
return {source, sink, dispose};
229+
return {source, sink, events, dispose};
227230
});
228231
```
229232

233+
**source** is an instance of ReactSource from this library, provided to the `main` so that events can be selected in the intent.
234+
235+
**sink** is the stream of ReactElements your `main` creates, which should be rendered in the component we're creating.
236+
237+
**events** is a *subset* of the sinks, and contains streams that describe events that can be listened by the parent component of the `CycleApp` component. For instance, the stream `events.save` will emit events that the parent component can listen by passing the prop `onSave` to `CycleApp` component. This `events` object is optional, you do not need to create it if this component does not bubble events up to the parent.
238+
239+
**dispose** is a function `() => void` that runs any other disposal logic you want to happen on componentWillUnmount. This is optional.
240+
230241
Use this API to customize how instances of the returned component will use shared resources like non-rendering drivers. See recipes below.
231242

232243
</p>
@@ -246,6 +257,9 @@ function makeComponent(main, drivers, channel = 'react') {
246257
const program = setup(main, {...drivers, [channel]: () => new ReactSource()});
247258
const source = program.sources[channel];
248259
const sink = program.sinks[channel];
260+
const events = {...program.sinks};
261+
delete events[channel];
262+
for (let name in events) if (name in drivers) delete events[name];
249263
const dispose = program.run();
250264
return {source, sink, dispose};
251265
});
@@ -268,6 +282,8 @@ function makeComponentReusing(main, engine, channel = 'react') {
268282
const sources = {...engine.sources, [channel]: source};
269283
const sinks = main(sources);
270284
const sink = sinks[channel];
285+
const events = {...sinks};
286+
delete events[channel];
271287
const dispose = engine.run(sinks);
272288
return {source, sink, dispose};
273289
});

src/convert.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ import {
88
createElement,
99
ComponentType,
1010
} from 'react';
11-
import {Stream} from 'xstream';
11+
import {Stream, Subscription} from 'xstream';
1212
import {ScopeContext} from './context';
1313
import {Sources, FantasySinks, Drivers, setup} from '@cycle/run';
1414
import {ReactSource} from './ReactSource';
1515
import {StreamRenderer} from './StreamRenderer';
1616

17-
type RunOnDidMount = () => {
17+
type CycleReactEngine = {
1818
source: ReactSource;
1919
sink: Stream<ReactElement<any>>;
20+
events?: {[name: string]: Stream<any>};
2021
dispose?: () => void;
2122
};
2223

24+
type RunOnDidMount = () => CycleReactEngine;
25+
2326
type State = {
2427
source: ReactSource | null;
2528
sink: Stream<ReactElement<any>> | null;
@@ -32,17 +35,37 @@ export function makeCycleReactComponent<P = any>(
3235
constructor(props: P) {
3336
super(props);
3437
this.state = {source: null, sink: null};
38+
this._subs = [];
3539
}
3640

3741
public _dispose?: () => void;
42+
public _subs: Array<Subscription>;
3843

3944
public componentDidMount() {
40-
const {source, sink, dispose} = run();
45+
const {source, sink, events, dispose} = run();
4146
source._props$._n(this.props);
4247
this._dispose = dispose;
48+
if (events) {
49+
this._subscribeToEvents(events);
50+
}
4351
this.setState({source, sink});
4452
}
4553

54+
public _subscribeToEvents(events: Pick<CycleReactEngine, 'events'>) {
55+
if (!events) return;
56+
for (let name in events) {
57+
if (!events[name]) continue;
58+
const handlerName = `on${name[0].toUpperCase()}${name.slice(1)}`;
59+
this._subs.push(
60+
events[name].subscribe({
61+
next: x => {
62+
if (this.props[handlerName]) this.props[handlerName](x);
63+
},
64+
}),
65+
);
66+
}
67+
}
68+
4669
public render() {
4770
const {source, sink} = this.state;
4871
if (!source || !sink) return null;
@@ -61,6 +84,9 @@ export function makeCycleReactComponent<P = any>(
6184

6285
public componentWillUnmount() {
6386
if (this._dispose) this._dispose();
87+
for (const sub of this._subs) {
88+
sub.unsubscribe();
89+
}
6490
}
6591
};
6692
}
@@ -82,14 +108,22 @@ export function makeComponent<
82108
} as any);
83109
const source: ReactSource = program.sources[channel];
84110
const sink = program.sinks[channel];
111+
const events = {...(program.sinks as object)};
112+
delete events[channel];
113+
for (let name in events) {
114+
if (name in drivers) delete events[name];
115+
}
85116
const dispose = program.run();
86-
return {source, sink, dispose};
117+
return {source, sink, events, dispose};
87118
});
88119
} else {
89120
return makeCycleReactComponent<P>(() => {
90121
const source = new ReactSource();
91-
const sink = main({[channel]: source} as any)[channel];
92-
return {source, sink};
122+
const sinks = main({[channel]: source} as any);
123+
const events = {...(sinks as object)};
124+
delete events[channel];
125+
const sink = sinks[channel];
126+
return {source, sink, events};
93127
});
94128
}
95129
}

test/conversion.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,35 @@ describe('Conversion', function() {
112112
renderer.create(createElement(RootComponent, {name: 'Alice', age: 30}));
113113
});
114114

115+
it('output React component routes other sinks to handlers in props', done => {
116+
function main(sources: {react: ReactSource}) {
117+
return {
118+
react: xs.of(
119+
h('section', [h('div', {}, [h('h1', {}, 'Hello world')])]),
120+
),
121+
something: xs
122+
.periodic(200)
123+
.mapTo('yellow')
124+
.take(1),
125+
};
126+
}
127+
128+
const RootComponent = makeCycleReactComponent(() => {
129+
const source = new ReactSource();
130+
const sinks = main({react: source});
131+
const sink = sinks.react;
132+
return {source, sink, events: {something: sinks.something}};
133+
});
134+
renderer.create(
135+
createElement(RootComponent, {
136+
onSomething: x => {
137+
assert.strictEqual(x, 'yellow');
138+
done();
139+
},
140+
}),
141+
);
142+
});
143+
115144
it('sources.react.props() evolves over time as new props come in', done => {
116145
function main(sources: {react: ReactSource}) {
117146
let first = false;

0 commit comments

Comments
 (0)