remcycle
v0.2.21-beta.1
Published
a Cycle.js-based component factory and set of higher-order-components built on cycle-react-driver, cycle-redux-driver, and most
Downloads
22
Readme
remcycle
a Cycle.js-based component factory and set of higher-order-components built on cycle-react-driver, cycle-redux-driver, and most
installation
npm install --save remcycle
usage
import mapProps from 'remcycle/es2015/hoc/mapProps'; // HOCs
import createComponent from 'remcycle/es2015/createComponent'; // helpers
import { shallowEquals } from 'remcycle/es2015/util'; // utilities
api
A higher-order component (HOC) is a function that takes in a Cycle.js component and returns a Cycle.js component.
This pattern makes HOCs composable and allows us to use Ramda's compose
or pipe
functions to stitch multiple HOCs together into a single, larger HOC.
The following HOC factories and utilities are provided by this library:
- Prop-related HOCs:
- Action-related HOCs:
- Helper and other HOCs:
- Utilities:
NOTE: A few HOC factories have two versions, one that exposes single props
or action
objects and another that exposes the raw props
source stream, action
streams and component sources - use the latter for more granular control over stream manipulation and creation.
mapProps
mapProps(propsMapper: (props: {}) => {}): HigherOrderComponent
Transforms props
to be passed into the base component as the props
source stream.
Useful as a base HOC for higher-level HOCs for transforming props (e.g. renaming, omitting, etc).
example:
mapProps(({ count, ...props }) => ({
value: count, // renames the count prop, omits the rest from the props source stream
}));
mapPropsStream
mapPropsStream(
propsStreamMapper: (propsSource: Stream<{}>) => Stream<{}>,
): HigherOrderComponent
Same as mapProps
, but transforms the props
source stream (or of({})
if props
source is undefined
).
Useful as a base HOC for higher-level HOCs for more granular control over transforming props (e.g. throttling, filtering, etc).
example:
mapPropsStream(propsSource => {
return propsSource
.map(({ count, ...props }) => ({
value: count, // same example as above
}));
});
withProps
withProps(
propsOrPropsCreator: {} | (props: {}) => {} | void,
): HigherOrderComponent
withProps(
watchedPropNames: string | string[],
propsCreator: (props: {}) => {} | void,
): HigherOrderComponent
Merges into the props
stream new props
, either created from an object or a function that returns new props
.
Either:
- accepts
props
to be merged with upstreamprops
, or a function that transforms upstreamprops
into newprops
to be merged - accepts name(s) of
props
to watch for changes (usingshallowEquals
), and when they have changed, run thepropsCreator
on upstreamprops
to return newprops
to be merged.
If propsCreator
returns undefined
, upstream props
are emitted unchanged.
Useful for adding new props
, overwriting upstream props
, or for merging in new props
that are expensive to create via the propsCreator
.
NOTE: the watching behaviour is only included in this HOC factory for backwards-compatibility reasons - if you want to control when you create new props
, you should use withPropsOnChange
.
example:
// merging props from props object
withProps({ name: 'Selector' });
// merging props from props creator
withProps(({ val }) => ({ value: val })); // simple renaming of props
// merging props from props creator on props change
withProps(
[ 'params.prop1', 'dependencyProp' ], // can take multiple and/or nested prop names
({ dependencyProp }) => {
// ... perform expensive operations here
return {
// ... new props to merge into upstream props
};
},
);
withPropsOnChange
withPropsOnChange(
namesOrPredicate: string | string[] | (currentProps: {}, previousProps: {}) => boolean,
propsCreator: (props: {}) => {} | void,
): HigherOrderComponent
Accepts name(s) of props
to watch for changes (using shallowEquals
) or a predicate
function applied to currentProps
and previousProps
, and when the listed props
have changed (or when the predicate
returns true
), run the propsCreator
on upstream props
to return new props
to be merged.
If propsCreator
returns undefined
, upstream props
are emitted unchanged.
Useful for merging in new props
that are expensive to create via the propsCreator
.
example:
// merging props from props creator on props change
withPropsOnChange(
[ 'params.prop1', 'dependencyProp' ], // can take multiple and/or nested prop names
({ dependencyProp }) => {
// ... perform expensive operations here
return {
// ... new props to merge into upstream props
};
},
);
// merging props from props creator based on predicate's return value
withPropsOnChange(
({ dependencyProp: current }, { dependencyProp: previous }) => {
// only when this prop is unequal to it's previous value
return current !== previous;
},
({ dependencyProp: current }) => {
// ... perform expensive operations here
return {
// ... new props to merge into upstream props
};
},
);
withState
withState<T>(
stateName: string,
initialStateOrCreator: T | (props: {}) => T,
actionReducers?: {
[actionType: string]:
(state: T, action: FluxStandardAction<any>, props: {}) => T,
},
propReducers?: {
[propNames: string | string[]]:
(state: T, props: {}) => T,
},
): HigherOrderComponent
Creates, merges into the props
source stream, and maintains the state of a new/existing prop
using the reducer pattern.
initialStateOrCreator
is either the initial state of the prop
or a function that creates the initial state from the current props
.
actionReducers
select the REDUX
source stream using the given actionType
and on each action, invokes the reducer on the state
, the action
, and the current props
.
propReducers
watch the given propName(s)
(possibly nested) for changes (using shallowEquals
) and when changed, invokes the reducer on the state
and current props
.
example:
withState(
'count',
({ count }) => count !== undefined ? count : 0,
{
'Counter/increment': (count, { payload }) => count += payload,
'Counter/decrement': (count, { payload }) => count -= payload,
'Counter/reset': _ => 0,
}, {
count: (_, { count }) => count // parent is resetting the state
},
);
defaultProps
defaultProps({ [propName: string]: any }): HigherOrderComponent
Merges in a set of props
, unless they already exist in the props
source stream. Like withProps
, but upstream props
already in the props
source stream take precedence.
example:
// if parent didn't provide a `value` prop for the <input type="number" /> tag, set it to 0 as a reasonable default
defaultProps({
type: 'number',
value: 0,
});
omitProps
omitProps(string | string[]): HigherOrderComponent
Omits props from the props
source stream.
example:
omitProps([ 'isLoading' ]); // omits the prop `isLoading`, perhaps because it wasn't needed
doOnPropsChange
doOnPropsChange(
propsCreator: (props: {}) => {} | void,
): HigherOrderComponent
doOnPropsChange(
watchedPropNames: string | string[],
propsCreator: (props: {}) => {} | void,
): HigherOrderComponent
doOnPropsChange(
namesOrPredicate: string | string[] | (currentProps: {}, previousProps: {}) => boolean,
propsCreator: (props: {}) => {} | void,
): HigherOrderComponent
Syntactically-identical to withProps
or withPropsOnChange
, but is recommended to be used when performing imperative/mutable/impure changes to existing props
.
mapActions
mapActions<T>(
{ [actionType: string]:
(action: FluxStandardAction<T>, props: {}) => FluxStandardAction<T> | void
}
): HigherOrderComponent
Transforms an action
of the given actionType
with the current props
. If the transform returns undefined
, the action
is filtered out of the action
stream.
Useful for augmenting action
payloads or filtering out action
s with invalid or undesirable payloads.
example:
// filters out any values greater than the `maxAllowable` for the <input type="number"/> tag
mapActions({
'Input/change': (action, { maxAllowable }) => {
return (Number(action.payload) > maxAllowable) ? void 0 : action;
},
});
mapActionStreams
mapActionStreams<T>(
{ [actionType: string]:
(action: Stream<FluxStandardAction<T>>, sources: {}) => Stream<FluxStandardAction<T>>
}
): HigherOrderComponent
Identical to mapActions
, except the action
stream creator function takes the action
stream of the given actionType
and all of the component's sources, returning the new action
stream.
example:
// same example as above
// filters out any values greater than the `maxAllowable` for the <input type="number"/> tag
mapActionStreams({
'Input/change': (action$, { props: propsSource = of({}) }) => {
const maxAllowable$ = propsSource.map(({ maxAllowable }) => maxAllowable);
return action$
.sample((action, maxAllowable) => ([ action, maxAllowable ]), action$, maxAllowable$)
.filter(([ { payload }, maxAllowable ]) => Number(payload) <= maxAllowable)
.map(([ action ]) => action)
.multicast();
},
});
withActions
withActions<T, U>(
{ [listenedActionType: string]:
{ [emittedActionType: string]:
(listenedAction: FluxStandardAction<T>, props: {}) => FluxStandardAction<U>
}
}
): HigherOrderComponent
Multiplexes an action
stream onto another action
stream - action
creator function takes in each listenedActionType
action and current props
and returns an action
of the emittedActionType
. If an action
stream of the emittedActionType
already exists, the returned action
s will be merged into that stream.
Useful for emitting an additional action
when a given action is emitted, or generally mapping a particular source to a new action
stream.
example:
// maps an Input component change action to a new action specific to the MaxInput component
withActions({
'Input/change': {
'MaxInput/set': ({ payload }) => setMaxInput(payload),
},
});
withActionStreams
withActionStreams<T, U>(
{ [listenedActionType: string]:
{ [emittedActionType: string]:
(listenedActionStream: Stream<FluxStandardAction<T>>, sources: {}) => Stream<FluxStandardAction<U>>
}
}
): HigherOrderComponent
Identical to withActions
, except the action
stream creator function takes the action
stream of the given listenedActionType
and all of the component's sources, returning the new action
stream. If an action
stream of the emittedActionType
exists, the returned action
stream will be merged into that stream.
example:
// same example as above, but debounces the emitted actions before emitting
// maps an Input component change action to a new action specific to the MaxInput component
withActionStreams({
'Input/change': {
'MaxInput/set': (inputChangeAction$, { Time }) => inputChangeAction$
.thru(Time.debounce(250))
.map(({ payload }) => setMaxInput(payload)),
},
});
mapSourcesToActionStreams
mapSourcesToActionStreams(
{ [actionType: string]: (sources: {}): ActionStream<any>; }
): HigherOrderComponent
Helper for mapping sources to new (or merge into existing) action
streams.
example:
// in event-sourcing parlance, we can map state changes to domain events
mapSourcesToActionStreams({
[INPUT_CHANGED]: ({ props: propsSource }) => {
return propsSource
.map(props => props['input'])
.map(inputChangedActionCreator)
.multicast();
},
});
addActionHandlers
addActionHandlers(
{ [handlerPropName: string]:
{ type: string,
actionCreator: (props: {}, eventArgs: any | any[]) => FluxStandardAction<any>,
actionStreamCreator: (sources: {}, event$: Stream<any>) => Stream<FluxStandardAction<any>>,
hold?: boolean,
}
}
): HigherOrderComponent
Similar to Redux's mapDispatchToProps
. addActionHandlers
does two things:
- merges into the
props
source stream a handler function of the providedhandlerPropName
- creates a
action
sink stream that emits anaction
when the handler is invoked.
If you declare the actionCreator
property, the actionCreator
function accepts the current props
and the eventArg
value passed into the handler (or an array of values if the handler is invoked with multiple arguments) and should return the action
of the desired type
.
If you declare the actionStreamCreator
property, the actionStreamCreator
function accepts the component's sources and the stream of eventArgs
emitted when the handler is invoked (again, with a single value or an array values, depending on the number of parameters passed into the handler) and should return a stream of actions
of the desired type
.
The hold
property should be specified as true
(default is false
) if the action
stream should remember it's last action
.
Useful for imperatively generating actions, most commonly from passing the handlers into a React component.
example:
// emits `'Input/change'` actions when the `onChange` handler is invoked, perhaps in an <input /> component
addActionHandlers({
onChange: {
type: 'Input/change',
actionCreator: (_, e) => inputChange(e.target.value),
},
});
mapView
mapView(
viewMapper: (vtree: ReactElement<any>, props: {}) => ReactElement<any>,
propsToPluck?: string | string[],
): HigherOrderComponent
Wraps a component's React element sink or it's child's React sink with additional React elements.
Optionally, allows you to specify props
to pluck from the props
source stream to use for rendering the wrapper component (defaults to '*'
which then passes all props
to the viewMapper
).
example:
const Container = ({ childVtree, className }) =>
<div className={className}>
{childVtree}
</div>;
mapView(
(vtree, { className }) => Container({ childVtree: vtree, className }),
[ 'className' ],
);
withCollection
withCollection(
collectionName: string,
initialCollectionOrCreator: Collection | (sources: {}) => Collection,
actionReducers?: {
[actionType: string]:
(collection: Collection, action: FluxStandardAction<any>, props: {}, sources: {}) => Collection,
},
propReducers?: {
[propNames: string | string[]]:
(collection: Collection, props: {}, sources: {}) => Collection,
},
): HigherOrderComponent
Creates and maintains the state of a Most.js Collection, returning a stream of the collection's state and merging it into the component's sources (so that it can be manipulated in subsequent HOCs).
initialCollectionOrCreator
is either the initial collection or a function that creates the initial collection from the sources.
actionReducers
select a REDUX
source stream using the given actionType
and on each action, invokes the reducer on the collection, the action
, the current props
and all sources.
propReducers
watch the given propName(s)
(possibly nested) for changes (using shallowEquals
) and when changed, invokes the reducer on the collection, current props
and all sources.
example:
// manages a collection of children `TodoItem` components and their state
withCollection('todoItems', Collection(TodoItem), {
'TodoList/add': (todos, { payload: text }) => {
const props$ = of({ text, isComplete: false });
return todos.add({ ...sources, props: props$ });
},
'TodoList/toggleAll': (todos, { payload: allChecked }) => {
return Array(todos.size).fill(0)
.reduce((newTodos, _, index) => {
const todo = todos.getAt(index);
const props$ = todo.input.props
.map(props => ({ ...props, isComplete: allChecked }));
return newTodos.setAt(index, { ...todo.input, props: props$ });
}, todos);
},
});
addActionTypes
addActionTypes(
componentName: string,
{ [actionType: string]: PropType }
): HigherOrderComponent
Augments the Redux action
sink, performing prop-type checks on each action
payload.
example:
addActionTypes('Input', {
'Input/change': PropTypes.string,
});
addPropTypes
addPropTypes(
componentName: string,
{ [propName: string]: PropType }
): HigherOrderComponent
Augments the props
source stream, performing prop-type checks on the props
object.
example:
addPropTypes('Input', {
value: PropTypes.string.isRequired,
type: PropTypes.string,
});
logActions
logActions(
sinkLogger: ({ [actionType: string]: Stream<FluxStandardAction<any>> }) => void,
actionLoggers: { [actionType: string]: (<FluxStandardAction<any>) => void },
): HigherOrderComponent
Takes in an optional sinksLogger
function which logs all action
streams in the Redux sink, and an object of logging functions for any given actionType
.
example:
logActions(::console.log, {
'Input/change': ::console.log,
});
logProps
logProps(
propsLogger: (props: {}) => void,
): HigherOrderComponent
Logs all props
from the upstream props
source stream.
example:
logProps(::console.log);
createComponent
createComponent({
main: Component;
name?: string;
handlers?: Handlers; // same input as `addActionHandlers`
children?: {
keys: string;
sources: HigherOrderComponent | HigherOrderComponent[];
sinks: HigherOrderComponent | HigherOrderComponent[];
};
wrapper?: HigherOrderComponent;
sinks?: HigherOrderComponent | HigherOrderComponent[];
sources?: HigherOrderComponent | HigherOrderComponent[];
actionTypes?: { [actionType: string]: PropType };
propTypes?: { [propName: string]: PropType };
isolate?: false | ((Sources: any) => (null | string | {})) | null | string | {};
}): Component
A factory function for creating Cycle.js components from a series of HOCs.
Each optional property results in an HOC and is applied in the following order:
main
: the base component to be wrappedhandlers
: React function callbacks to be passed asprops
(usually, only used withview
) with resultantaction
s emittedchildren
: object (soon to be array of objects) of HOCs to define what children should be rendered and how their collective state should be maintainedwrapper
: HOC defining any low level source or sink transformations required for this component or it's childrensinks
: HOC or array of HOCs describing sink transformationssources
: HOC or array of HOCs describing source transformationsisolate
: function returning the isolation configuration to be applied to all instances of this component
By applying these HOCs in this order, we get the resulting mental model:
- main:
- main takes in:
- most-manipulated sources
- least-manipulated sinks
- any children components as sources, returns them with sinks
- main takes in:
- handlers
- merges function callbacks into existing
props
- creates and emits raw
actions
(from theactionCreator
oractionStreamCreator
function) using the most-completeprops
- merges function callbacks into existing
- children
- receives most-manipulated sources
- merges/combines its own sinks with least-manipulated sinks
- wrapper
- performs any last-minute source manipulation for
main
and/orchildren
- provides lowest-level interface for sink manipulation
- performs any last-minute source manipulation for
- sinks
- performs manipulation of sinks with sources available from source HOCs
- sources
- performs manipulation of sources passed in from the parent components
example:
Say we want to wrap the great react-select Select
component...
import React from 'react';
import createComponent from 'remcycle/es2015/createComponent';
import { fromReactDOMComponent } from '@sunny-g/cycle-react-driver/es2015/dom';
const main = fromReactDOMComponent(Select, 'REACT'); // maps props to a React sink named 'REACT'
export default createComponent({
name: 'Select', // used for prop-type reporting
main: , // can be any Cycle component, but creating one from a React component
isolate: () => Math.random().toString(), // isolation with a random scope
handlers: {
onChange: { // necessary prop for receiving current selection
type: 'Select/change',
actionCreator: (_, selection) => ({
type: 'Select/change',
payload: selection,
error: false,
meta: {},
}),
},
},
sources: [
defaultProps({ // provides some react-select defaults
defaultValue: '',
multi: false,
name: 'Select',
}),
// manages state of the react-select component's `value`
withState('value', ({ defaultValue }) => defaultValue || null, {
'Select/change': (_, selection) => selection,
}),
],
sinks: [], // no need to manipulate sinks
propTypes: { // we can define expected `props` to be included in the `props` source
multi: PropTypes.bool,
name: PropTypes.string,
},
actionTypes: { // likewise, we can document the payload types of the `action`s this component emits
'Select/change': PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
},
});
reactSinksCombiner
type ReactSink = Stream<ReactElement<any>>;
reactSinksCombiner(
view: (...vtrees: ReactElement<any>[], props: {}) => ReactElement<any>,
propsSource?: Stream<{}>,
): (...reactSinks: ReactSink[]) => ReactSink
Helper for combining the React sinks of multiple sibling components into a single React sink, but can be used as a shorthand to combine
any set of sinks.
example:
import reactSinksCombiner from 'remcycle/es2015/reactSinksCombiner';
function main(sources) {
const todoItemOneSinks = TodoItem(sources);
const todoItemTwoSinks = TodoItem(sources);
// props required for the combined React view component
const viewProps$ = sources.props.map(({ className }) => ({ className }));
// the view that combines the vtrees and props
const TodoListView = (todoItemOneVtree, todoItemTwoVtree, { className }) =>
<div className={className}>
{todoItemOneVtree}
{todoItemTwoVtree}
</div>;
const mainReactSink = reactSinksCombiner(TodoListView, viewProps$)(todoItemOneSinks.REACT, todoItemTwoSinks.REACT);
return {
REACT: mainReactSink,
};
}
reduxSinksCombiner
type ReduxSink = Stream<{ [actionType: string]: Stream<FluxStandardAction<any>> }>
reduxSinksCombiner(...reduxSinks: ReduxSink[]): ReduxSink
Helper for merging the Redux sinks of multiple sibling components into a single Redux sink. If multiple action
streams of the same actionType
exist, the streams are merged together.
example:
import reduxSinksCombiner from 'remcycle/es2015/reduxSinksCombiner';
function main(sources) {
const todoItemOneSinks = TodoItem(sources);
const todoItemTwoSinks = TodoItem(sources);
const mainReduxSink = reduxSinksCombiner(todoItemOneSinks.REDUX, todoItemTwoSinks.REDUX);
return {
REDUX: mainReduxSink,
};
}
shallowEquals
shallowEquals(any, any): boolean
Determines if two JS value or reference types are shallowly equal to eachother (using ===
). If an array
or object
, strict equality is applied to all elements/properties. Directly exported from the shallow-equals library.
example:
import { shallowEquals } from 'remcycle/es2015/util';
shallowEquals({ a: 1 }, { a: 1 }); // true
shallowEquals({ a: 1 }, { b: 1 }); // false
contributing
todo
- ensure typescript typings are correct and comprehensive and exported correctly
- ensure build tooling with
tsc
andwebpack
is correct - explain contribution process
- add more tests :)
- explain why I wrote this
license
ISC