Compose reactive props for FrintJS Apps
With npm:
$ npm install --save rxjs frint-props
Via unpkg CDN:
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.0/Rx.min.js"></script>
<script src="https://unpkg.com/frint-props@latest/dist/frint-props.min.js"></script>
<script>
// available as `window.FrintProps`
</script>
The package consists of multiple functions that enable you to compose your props as an RxJS Observable.
There are two kinds of functions:
And then there is compose
function that accepts both kinds of functions as arguments, and returns a single function that you can use anywhere.
We can start small, and prepare a stream that only emits the props once, and it doesn't change over time:
import { withDefaults } from 'frint-props';
const defaultProps = {
foo: 'foo value',
};
const props$ = withDefaults(defaultProps)();
Now that we have the Observable available as props$
, we can subscribe to it as needed:
props$.subscribe(props => console.log(props));
But we have more real world use cases that require our props to change over time too. We can consider using withState
for this example:
import { withState } from 'frint-props';
const props$ = withState('counter', 'setCounter', 0)();
Now the props$
Observable will emit an object with these keys:
counter
(Integer
): The counter valuesetCounter(n)
(Function
): Call this function to update counter
valueYou can compose multiple functions together to generate a combined stream of all props. For that, we can use the compose
function:
import { compose, withDefaults, withState } from 'frint-props';
const props$ = compose(
withDefaults({ foo: 'bar' }),
withState('counter', 'setCounter', 0),
withState('nane', 'setName', 'FrintJS')
)();
The props$
Observable will now emit with an object with these keys:
foo
(String
)counter
(Integer
)setCounter(counter)
(Function
)name
(String
)setName(name)
(String
)As you call functions like setCounter
or setName
, it will emit a new object with updated values for counter
and name
.
Besides adding just props, you may also need to process the stream further.
For example, you may want to control how often the Observable emits new values. We can use shouldUpdate
function for this:
import { compose, withDefaults, withState, shouldUpdate } from 'frint-props';
const props$ = compose(
withDefaults({ counter: 0 }),
withState('counter', 'setCounter', 0),
shouldUpdate((prevProps, nextProps) => {
return prevProps.counter !== nextProps.counter;
})
)();
The implementation of shouldUpdate
above tells our props$
Observable to emit new values only if the counter
value has changed. Otherwise nothing new is emitted.
The API of creating your own functions is pretty simple.
A basic function that is concerned about adding new props can be written like this:
function withFoo() {
return function () {
return {
foo: 'foo value here',
};
};
}
You can also return an Observable instead of a plain object:
import { of } from 'rxjs/observable/of';
function withFoo() {
return function () {
return of({
foo: 'foo value here',
});
};
}
You would notice that instead of returning the Object/Observable directly from our function, we return another function which takes care of returning the final result.
This allows us to access additional arguments when composing props.
Imagine if we want to make the FrintJS app
instance available to our functions:
const app = new App();
const props$ = compose(
withFoo()
)(app);
The argument app
can now be accessed inside our function like this:
withFoo() {
return function (app) {
return {
foo: 'foo value here',
};
};
}
The way some of our functions are designed in this repository, the returned functions expect to receive the same arguments as our observe
higher-order component receives in frint-react
, which are: app
and parentProps$
.
Besides just adding new props, functions can also take care of processing the stream further just like how RxJS operators work.
We can create a function that will check if there is any foo
prop, and then capitalize it:
import { map } from 'rxjs/operators/map';
function capitalizeFoo() {
return function () {
return map((props) => ({
...props,
foo: props.foo
? props.foo.toUpperCase()
: undefined,
}));
};
}
Can be composed together as follows:
import { compose } from 'frint-props';
const props$ = compose(
withFoo(),
capitalizeFoo()
);
The props$
Observable will now emit { foo: 'FOO VALUE HERE' }
.
All the functions return a function, when called, returns an Observable of props.
withDefaults(defaultProps)
defaultProps
: Default props to start the stream withconst props$ = withDefaults({ foo: 'foo value here' })();
withState(valueName, setterName, initialValue)
valueName
(String
): Prop name for the valuesetterName
(String
): Prop name for the setter functioninitialValue
(any
): Initial value for the stateconst props$ = withState('counter', 'setCounter', 0)();
withStore(mapState, mapDispatch, options = {})
Works with frint-store
or Redux store set in FrintJS App as a provider.
mapState
(Function
OR null
): Maps state to propsmapDispatch
(Object
): Action creators keyed by namesoptions
(Object
) [optional]: Object with additional configurationoptions.providerName
(String
): Defaults to store
options.appName
(String
): Defaults to null
, otherwise name of any Child Appconst app = new App(); // assuming it has a `store` provider
const mapState = state => ({
foo: state.foo,
bar: state.bar
});
const mapDispatch = {
handleClick: () => ({
type: 'HANDLE_CLICK'
})
};
const props$ = withStore(mapState, mapDispatch)(app);
You can also pass null
in place of mapState
parameter if you don't want to subscribe to store updates.
const app = new App(); // assuming it has a `store` provider
const mapDispatch = {
handleClick: () => ({
type: 'HANDLE_CLICK'
})
};
const props$ = withStore(null, mapDispatch)(app);
withObservable(source$, ...mappers)
source$
(Observable
OR function returning Observable
)mapper
(Function
): Returning props OR Observable of propsimport { of } from 'rxjs/observable/of';
const props$ = withObservable(
of({ foo: 'foo value here' }),
props => ({ foo: props.foo.toUpperCase() }),
props => ({ foo: `${props.foo}!` })
)();
Generated from a function:
import { of } from 'rxjs/observable/of';
const props$ = withObservable(
() => of({ foo: 'foo value here' })
)();
withHandlers(handlers)
This function can be only used via compose
.
handlers
(Object
): Functions keyed by prop nameconst props$ = compose(
withHandlers({
handleClick: props => () => console.log('Clicked!')
})
)();
Other props are accessible too:
const props$ = compose(
withState('counter', 'setCounter', 0),
withHandlers({
increment: props => () => props.setCounter(props.counter + 1)
})
)();
Additional arguments can be accessed as follows:
const props$ = compose(
withHandlers({
handleClick: (props, arg1, arg2) => () => console.log('Clicked!')
})
)(arg1, arg2);
compose(...functions)
Composes multiple functions into a combined single function, that can be called later.
const props$ = compose(
withDefaults({}),
withState('counter', 'setCounter', 0),
withState('name', 'setName', 'FrintJS'),
shouldUpdate((prevProps, nextProps) => true)
)();
map(mapperFn)
mapperFn
(Function
): Function that accepts processed props, and returns new mapped props objectconst props$ = compose(
withDefaults({ foo: 'foo value' }),
map(props => ({ foo: props.foo.toUpperCase() }))
)();
Will emit { foo: 'FOO VALUE' }
.
pipe(operator)
Pipes with any RxJS operator.
operator
(Function
): RxJS operatorimport { map } from 'rxjs/operators/map';
const props$ = compose(
withDefaults({ foo: 'foo value' }),
pipe(map(props => ({ foo: props.foo.toUpperCase() })))
)();
shouldUpdate((prevProps, nextProps) => true)
Controls when to emit props.
Function
: receives previous and next props, and should return a Boolean deciding whether to update or not