diff --git a/README.md b/README.md index 0914f91..6527687 100644 --- a/README.md +++ b/README.md @@ -140,93 +140,85 @@ The library includes [basic implementations of this type](https://github.com/iva ## Lenses -First let's talk about how we define a lens. When building React components, it's convenient to work with a type which we'll call `StateView`, a combination of a value and a setter: +We start by defining a `View` as a combination of a getter and a setter: ```ts -type StateView = [value: A, set: (value: A) => void]; +type View = { get: () => A; set: (value: A) => S }; ``` -Values returned by React's `setState` hook can be treated as values of this type, and it is also what you would want to pass to an input element such as a textbox to create a two-way binding. In this library we actually define `StateView` as a subtype of another type called `View` (you'll soon see why): +and define a `Lens` as a function that transforms a view into another view: ```ts -type View = [value: A, set: (value: A) => S]; -type StateView = View; +type Lens = (source: View) => View; ``` -and we define a `Lens` as a function that transforms a view `View` into another view `View` (it follows that a lens will transform a `StateView` into another `StateView`). +The library provides the following functions: -To see how this works, we'll write a React component using the following two functions provided by the library: +- [`objectProp`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/object/objectProp.ts): a lens to zoom in on an object's property. -- [`objectProp`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/object/objectProp.ts): a lens which zooms in on an object's property, e.g. `objectProp('a')` will transform a value of type `StateView<{ a: number }>` into a value of type `StateView`. - -- [`bindingProps`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/react/bindingProps.ts): a helper function that converts a `StateView` into an object with props that React input components understand, e.g. `['x', set]` would be transformed into `{ value: 'x', onChange: ({ currentTarget: { value } }) => set(value) }`. - -Here's what the component will look like: - -```ts -type State = { a: string; b?: { c: string } }; - -/** - * A component that encapsulates presentation logic but is agnostic as to how we - * manage state. - */ -const StatelessComponent = ({ stateView }: { stateView: StateView }) => ( -
- {/* An input bound to 'a'. */} - - {applyPipe(stateView, objectProp('b'), ([value, set]) => - // If 'b' is absent,... - value === undefined ? ( - // ...a button that adds a default value for 'b',... - - ) : ( - // ...otherwise (if 'b' is present), an input bound to 'c'. - - ), - )} -
-); - -export const StatefulComponent = () => { - const stateView = React.useState({ a: '' }); - return ; -}; -``` +- [`mapProp`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/map/mapProp.ts): a lens to zoom in on a value stored in a `Map`. -In the code above, TypeScript successfully infers the types, and as we get to a point where we need to type 'a', 'b', or 'c', IntelliSense shows correct suggestions. +- [`setProp`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/set/setProp.ts): a lens to zoom in on presence of an element in a `Set`. -Checkbox is different from other inputs in that we have to use `checked` prop instead of `value`, so when binding a checkbox, instead of `bindingProps` use [`bindingPropsCheckbox`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/react/bindingPropsCheckbox.ts). +- [`rootView`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/view/rootView.ts): a function that converts a `value` into a view `{ get: () => value, set: }`. -In the component example we used `objectProp` lens to transform a `StateView` into another `StateView`, but like other lenses, it also works on `StateView`'s supertype `View`. Thanks to that, we can use `objectProp` in the conventional way to immutably set a property nested within a larger structure, as in the following example of a reducer that sets the value of `b` in `{ a: { b: string; c: string } }`: +Example usage: ```ts type State = { a: { b: string; c: string } }; +/** + * A reducer that sets the value of `b` in the state to the payload. + **/ const sampleReducer = (state: State, action: { payload: string }) => applyPipe( - [state, (value) => value] as View, + // Returns `View`. + rootView(state), // Transforms values into `View`. objectProp('a'), // Transforms values into `View`. objectProp('b'), + ) // `set` takes a value for `b` and returns a new `State`. - ([, set]) => set(action.payload), - ); + .set(action.payload); expect(sampleReducer({ a: { b: '', c: '' } }, { payload: 'x' })).toEqual({ a: { b: 'x', c: '' }, }); ``` -There is a simple helper function [`rootView`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/view/rootView.ts) which converts a `value` into a view `[value, ]` and which we can use to replace the first argument in the `applyPipe` call above, including the type signature, with just `rootView(state)`. +Example of usage with optional properties: -The only other lens-related utilities that are left to mention are: +```ts +// Note the optional `a`. +type State = { a?: { b: string; c: string } }; -- [`mapProp`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/map/mapProp.ts): a lens to zoom in on a value stored in a `Map`. +const sampleReducer = (state: State, action: { payload: string }) => + applyPipe( + rootView(state), + // Transforms values into `View`. + objectProp('a'), + // Transforms values into `View`. + ({ get, set }) => ({ + get: () => get() ?? { b: '', c: '' }, + set, + }), + objectProp('b'), + ).set(action.payload); -- [`setProp`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/set/setProp.ts): a lens to zoom in on presence of an element in a `Set`. +expect(sampleReducer({}, { payload: 'x' })).toEqual({ + a: { b: 'x', c: '' }, +}); +``` + +The library also defines + +```ts +type StateView
= View; // = { get: () => A; set: (value: A) => void }; +``` + +where the setter does not return any value, but instead produces a side effect. To learn about how this type is used, please see the docs for [`antiutils-react`](https://github.com/ivan7237d/antiutils/blob/master/src/internal/view/rootView.ts), a package that provides glue between Antiutils and React. ## Memoization diff --git a/src/internal/object/objectProp.test.ts b/src/internal/object/objectProp.test.ts index 29328b2..529d129 100644 --- a/src/internal/object/objectProp.test.ts +++ b/src/internal/object/objectProp.test.ts @@ -105,27 +105,12 @@ it('works with index signatures', () => { it('works in example 1 from README', () => { type State = { a: { b: string; c: string } }; + /** + * A reducer that sets the value of `b` in the state to the payload. + **/ const sampleReducer = (state: State, action: { payload: string }) => applyPipe( - { get: () => state, set: (value) => value } as View, - // Transforms values into `View`. - objectProp('a'), - // Transforms values into `View`. - objectProp('b'), - ) - // `set` takes a value for `b` and returns a new `State`. - .set(action.payload); - - expect(sampleReducer({ a: { b: '', c: '' } }, { payload: 'x' })).toEqual({ - a: { b: 'x', c: '' }, - }); -}); - -it('works in example 2 from README', () => { - type State = { a: { b: string; c: string } }; - - const sampleReducer = (state: State, action: { payload: string }) => - applyPipe( + // Returns `View`. rootView(state), // Transforms values into `View`. objectProp('a'), @@ -140,7 +125,7 @@ it('works in example 2 from README', () => { }); }); -it('works in example 3 from README', () => { +it('works in example 2 from README', () => { type State = { a?: { b: string; c: string } }; const sampleReducer = (state: State, action: { payload: string }) =>