# FRP tools Property, Container, Computed and Stream value stores designed to work together for storing discrete and derived state. # [property](./src/property.js) A `property` is a simple value store that can report when its value changes. It is good for wrapping external values passed into a component so compute types can depend on them and only recompute when these values change. It can also be used to receive events such as _window.onresize_ to always provide the current viewport size. ## Usage ### Creation Creates and sets initial value to `true` ```js const inViewport = prop(true); ``` ### Read Call it to receive the stored value. ```js if (inViewport()) { /* inViewport is truthy */ } ``` ### Change Call it passing the new value. If any computed stores depend on this value they will be marked dirty and re-evaluated the next time they are read from. ```js inViewport(false); ``` ### Provide a hash function for complex types When storing a type that is not determined to be equal with simple equality (===), provide a hash function to be used for simple comparison to determine if the new provided value should be propagated to dependents. ```js function hashSet(_a) { if (_a instanceof Set) { return Array.from(_a.keys()) .sort() .map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`) .join('?'); } return _a; } const a = prop(new Set([1, 2]), hashSet); ``` # [computed](./src/computed.js) `computed` is a functional store that depends on the values of properties or other computeds. They derive value from properties rather than store value and hence cannot be set directly. ## Behavior A `computed` will subscribe to its dependencies in such a way that it will be marked as _dirty_ when any dependency changes. Whenever it is read from, if will recompute its result if the _dirty_ flag is set, otherwise it just return the stored result from the last time it computed. ## Usage ### Creation ```js const showDialog = computed( (inVP, shouldShow) => inVP && shouldShow, // computation function [inViewport, shouldShow] // array of dependencies, can be either a property or computed ); ``` ### Read ```js if (showDialog()) { /* showDialog() is truthy */ } ``` Call it to receive the stored value, recomputing if necessary. ### Subscribe to changes Call the subscribe method with a callback that will be called when the computed result changes to a different value. The returned function can be called to unsubscribe from the property. When called it will provide the count of remaining subscriptions. ```js const unsubscribe = inViewport.subscribe(console.log.bind(console)); const remainingSubscriptionCount = unsubscribe(); ``` **NOTE**: Subscribing to a computed forces it to recompute every time an upstream dependency changes. This could negatively performance if it depends on multiple values that change sequentially and the computation function is non-trivial. For example: ```js const inViewport = prop(false); const shouldShow = prop(false); const showDialog = computed((inVP, shouldShow) => inVP && shouldShow, [inViewport, shouldShow]); inViewport(true); // showDialog marked as dirty but does not recompute its stored result. shouldShow(true); // showDialog is already marked as dirty. Nothing else happens. showDialog(); // showDialog recomputes its stored result and unsets the dirty flag. // adding a subscription will change showDialog's internal behavior showDialog.subscribe(console.log.bind(console)); inViewport(false); // showDialog result recomputed and `false` is written to the console. shouldShow(false); // showDialog result recomputed, console.log is not called. showDialog(); // showDialog does not recompute, console.log is not called. `false` is returned. showDialog.detach(); // Call detach to remove this computed from the logic tree. showDialog.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions. ``` ### Provide a hash function for complex types When the computed result is a type that is not determined to be equal with simple equality (===), provide a hash function to be used for simple comparison to determine if the new provided value should be propagated to dependents. ```js function hashSet(_a) { if (_a instanceof Set) { return Array.from(_a.keys()) .sort() .map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`) .join('?'); } return _a; } function _intersection(a, b) { return new Set([...a].filter(x => b.has(x))); } const a = prop(new Set([1, 2]), hashSet); const b = prop(new Set([2, 3]), hashSet); const intersection = computed(_intersection, [a, b], hashSet); ``` # [stream](./src/stream.js) `stream` is a computed that works asynchronously. Dependencies can be synchronous or asynchronous functions but the hash function remains synchronous. Also calling a stream multiple overlapping times will return the same promise and result (so long as dependencies do not change in the time gap). ## Usage ### Creation ```js const getToken = stream( async tokenUrl => fetch(tokenUrl), // computation function [getTokenUrl, tokenTimeout] // array of dependencies, can be a property, computed or stream ); ``` ### Read ```js if (await getToken()) { /* do stuff with the token */ } ``` Call it to receive the stored value, recomputing if necessary. # [container](./src/container.js) `container` is a wrapper around any container type (Object, Set, Map, or Array) while monitoring changes to the container. A container can be subscribed to and `computed` instances can depend on them. ## Behavior Anytime a property is set or a method is gotten and called, the container will check for an updated state and trigger subscribers if it is updated. An optional hash function may be applied to determine updated status. If the hash function is not supplied, every update will be propagated. ## Usage ### Creation ```js const monkeys = contained([], arr => arr.join('$')); const firstMonkey = computed(m => (m.length ? m[0] : null), [monkeys]); firstMonkey.subscribe(console.log.bind(console)); ``` ### Add a member to the container ```js monkeys.push('Bill'); ``` _firstMonkey_ would be computed and "Bill" would be logged to the console. ### Access the contained object directly Reference the `_` (underscore) property to access the contained object directly. ```js monkeys._.push('Bill'); ``` The array in _monkeys_ would get a new value without _firstMonkey_ being notified of the change. # Common Behaviors All frptools types have the following methods available: ## `.subscribe(fn)` - Subscribe to changes Call the `subscribe` method with a callback that will be called when the property value changes. The returned function can be called to unsubscribe from the property. When called it will provide the count of remaining subscriptions. ```js const unsubscribe = inViewport.subscribe(console.log.bind(console)); const remainingSubscriptionCount = unsubscribe(); ``` ## `.unsubscribeAll()` - Remove child subscriptions Call the `unsubscribeAll` method to remove all child-node subscriptions. ```js const unsubscribe = inViewport.subscribe(console.log.bind(console)); const remainingSubscriptionCount = unsubscribe(); inViewport.unsubscribeAll(); // console.log will no longer be called. ``` ## `.fire(val)` - Send a value to the node's subscribers Call the `fire` method to send a value to each of the node's subscribers. This is designed for the node to use to propagate updates but firing other values could have some uses. ```js const unsubscribe = inViewport.subscribe(console.log.bind(console)); inViewport.fire(false); // "false" logged to console. ``` # Utilities ## id `id(anything) -> anything` `id` is a function that returns its first parameter. It is used as the default hash function for each subscribable. ## call `call(anything) -> void` `call` will call the first parameter if it is a function. It is used for iterating over subscribables. ## pick `pick(propName, default) -> (obj) -> any` `pick` returns a function that accepts an object and will return the object's property for the provided name or the default if one is supplied. `pick` is not directly used with any subscribable but can be useful as the computed function when breaking down a `prop` that contains an object or a container. # Inspiration FRPTools is the result of years of learning from the following projects: - [KnockoutJS](http://knockoutjs.com/) - [Overture](https://github.com/fastmail/overture) - [Redux](https://redux.js.org/) - [Mithril](https://mithril.js.org/) # Possible Future Plans - Invert the param signature to allow currying the hash function in to create a sort of "type".