diff --git a/.travis.yml b/.travis.yml index 4d946f15..2cd349e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ jobs: include: - stage: Check compilation node_js: node - script: npm build + script: npm run build - stage: Produce Coverage node_js: node diff --git a/docs/README.md b/docs/README.md index 97d0d6cb..eac36338 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ --- id: redux-leaves -title: Core API +title: reduxLeaves hide_title: true -sidebar_label: reduxLeaves API +sidebar_label: reduxLeaves --- # `reduxLeaves(initialState, [reducersDict = {}])` @@ -13,7 +13,7 @@ Returns a reducer function and an object. ## Parameters - [`initialState`](#initialstate) *(object)*: the state shape and initial values for your Redux store -- [`reducersDict`](#reducersdict) *(object, optional)*: a collection of [leaf reducers](leafReducers.md) keyed by their [creator keys](creatorKeys.md) +- [`reducersDict`](#reducersdict) *(object, optional)*: a collection of [leaf reducers](api/leafReducers.md) keyed by their [creator keys](api/creatorKeys.md) ### `initialState` *(object)* @@ -38,8 +38,8 @@ const initialState = { *(object)* This is an object where every `key`-`value` pair is such that: -- `value` *(function | object)* is a [leaf reducer](leafReducers.md); -- `key` is a [creator key](creatorKeys.md) for that leaf reducer. +- `value` *(function | object)* is a [leaf reducer](api/leafReducers.md); +- `key` is a [creator key](api/creatorKeys.md) for that leaf reducer. #### Example @@ -56,90 +56,14 @@ const reducersDict = { ## Returns `array`, with two elements: - 0th: `reducer` *(function)*: a reducer function to pass to redux's `createStore` -- 1st: `actions` *(object)*: an object with same shape as `initialState` +- 1st: [`actions`](api/actions.md) *(object)*: an object with same shape as `initialState` ### `reducer` The root reducer for your Redux store. -It listens to actions created through [`actions`](#actions) at a given leaf for a given [creator key](creatorKeys.md), and updates that leaf's state using the [leaf reducer](leafReducers.md) keyed by the creator key. +It listens to actions created through [`actions`](api/actions.md) at a given leaf for a given [creator key](api/creatorKeys.md), and updates that leaf's state using the [leaf reducer](api/leafReducers.md) keyed by the creator key. ### `actions` -An object with nested action creator functions, following the same shape as the `initialState` passed to `reduxLeaves`. - -#### Example - -First, grab `reducer` and `actions` by destructuring the return value of `reduxLeaves`: - -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - counter: 1, - foo: ['bar'] - nested: { - deep: {} - state: { - manageable: 'maybe...?' - } - } -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` - -`actions` is an object with the same shape as `initialState`, with corresponding branches and leaves. - -Every action branch and leaf is an object, regardless of the initial data type: - -```js -console.log(typeof actions.counter) // 'object' -console.log(typeof actions.nested.deep) // 'object' -console.log(typeof actions.nested.state.manageable) // 'object' -``` - -Every action branch and leaf also has an additional property, `create`, that is an object: - -```js -console.log(typeof actions.counter.create) // 'object' -console.log(typeof actions.nested.deep.create) // 'object' -console.log(typeof actions.nested.state.manageable.create) // 'object' -``` - -This `create` property is the API through which you can access action creators. - -For example, suppose I want to dispatch an action that will increment my `counter` state by 2. - -```js -// We can destructure to access the 'increment' method from the create API -const { increment } = actions.counter.create - -// Increment is an action creator function which takes one argument -const action = increment(2) - -// Dispatch the action to the store -store.dispatch(action) - -// The store's state changes as expected! -console.log(store.getState()) -/* -* { -* counter: 3, <--------------- incremented by 2! -* foo: ['bar'], -* nested: { -* deep: {}, -* manageable: 'maybe...?' -* } -* } -*/ -``` -This API allows for concise but descriptive dispatching of actions. -```js -// Push 'FOO' to the 'foo' (array) slice of state -// by creating and dispatching an action - -store.dispatch(actions.foo.create.push('FOO')) -``` \ No newline at end of file +See documentation on the [`actions`](api/actions.md) object. \ No newline at end of file diff --git a/docs/api/actions.md b/docs/api/actions.md new file mode 100644 index 00000000..13c31e26 --- /dev/null +++ b/docs/api/actions.md @@ -0,0 +1,67 @@ +--- +id: actions +title: actions +hide_title: true +sidebar_label: actions +--- + +# `actions` + +## Arbitrary property paths + +The `actions` object returned by `reduxLeaves` can take an arbitrary path of properties after it, which typically correspond to a 'leaf' at the corresponding path from your state. + +```js +import reduxLeaves from 'redux-leaves' + +const initialState = { + counter: 0, + arbitrary: { + nested: { + path: ['hi!'] + } + } +} + +const [reducer, actions] = reduxLeaves(initialState) + +// state.counter +console.log(typeof actions.counter) // 'object' + +// state.arbitrary.nested +console.log(typeof actions.arbitrary.nested) // 'object' + +// state.arbitrary.nested.path +console.log(typeof actions.arbitrary.nested.path) // 'object' + +// but also works for paths not in your initial state +console.log(typeof actions.not.in.my.initial.state) // 'object' +``` + +## Accessing `create` + +For a given arbitrary property path, you have two options: navigating to a deeper property / leaf, or accessing the [`create`](create.md) property. + +```js +// Access create at the state root +console.log(typeof actions.create) // 'function' + +// Go deeper +console.log(typeof actions.arbitrary) // 'object' + +// Access create at state.arbitrary +console.log(typeof actions.arbitrary.create) // 'function' + +// Go deeper +console.log(typeof actions.arbitrary.nested.path) + +// Access create at state.arbitary.nested.path +console.log(typeof actions.arbitrary.nested.path.create) // 'function' +``` + +Once you've accessed `create`, you can't go arbitrarily deeper beyond that. +```js +console.log(typeof actions.create.arbitrary) // 'undefined' +``` + +This is because the `create` key accesses the Redux-Leaves [action creator (`create`) API](create.md) at the corresponding leaf of state. \ No newline at end of file diff --git a/docs/api/actions.spec.js b/docs/api/actions.spec.js new file mode 100644 index 00000000..dbc12241 --- /dev/null +++ b/docs/api/actions.spec.js @@ -0,0 +1,33 @@ +import reduxLeaves from '../../src'; + +describe('actions can take an arbitrary path of properties after it', () => { + const initialState = { + counter: 0, + arbitrary: { + nested: { + path: ['hi!'] + } + } + } + + const [reducer, actions] = reduxLeaves(initialState) + + test('Any arbitrary path returns an object', () => { + expect(typeof actions.counter).toBe('object') + expect(typeof actions.arbitrary.nested).toBe('object') + expect(typeof actions.arbitrary.nested.path).toBe('object') + expect(typeof actions.not.in.my.initial.state).toBe('object') + }) + + test('Any arbitrary path has the create function property', () => { + expect(typeof actions.create).toBe('function') + expect(typeof actions.counter.create).toBe('function') + expect(typeof actions.arbitrary.nested.create).toBe('function') + expect(typeof actions.arbitrary.nested.path.create).toBe('function') + expect(typeof actions.not.in.my.initial.state.create).toBe('function') + }) + + test("You can't access arbitrary paths from create", () => { + expect(typeof actions.create.arbitrary).toBe('undefined') + }) +}) \ No newline at end of file diff --git a/docs/api/create.md b/docs/api/create.md new file mode 100644 index 00000000..30c83959 --- /dev/null +++ b/docs/api/create.md @@ -0,0 +1,100 @@ +--- +id: create +title: create +hide_title: true +sidebar_label: create +--- + +# `create` + +When you access the `create` property from any arbitrary path from the [`actions`](actions.md) object, you access the Redux-Leaves action creators API. + +Consider the `actions` object returned below. +```js +import { createStore } from 'redux' +import reduxLeaves from 'redux-leaves' + +const initialState = { + counter: 0, + arbitrary: { + nested: { + path: ['hi!'] + } + } +} + +const reducersDict = { + convertToFoobar: () => 'foobar' +} + +const [reducer, actions] = reduxLeaves(initialState, reducersDict) +``` + +* `actions.counter.create` corresponds to creating actions at `state.counter`; +* `actions.arbitrary.nested.path.create` corresponds to creating actions at `state.arbitrary.nested.path`; +* `actions.create` corresponds to creating actions at the `state`'s root. + +## `create[creatorKey]` +### Action creators +You can access action creators (functions) from the `create` API at any leaf by using their [`creatorKey`](creatorKeys.md) as a property, both for the default ones and any custom ones you've defined. + +```js +// Example defaults: update, set, push +console.log(typeof actions.counter.create.update) // 'function' +console.log(typeof actions.arbitrary.nested.create.set) // 'function' +console.log(typeof actions.arbitrary.nested.path.create.push) // 'function' + +// Custom creatorKey of 'convertToFoobar' +console.log(typeof actions.create.convertToFoobar) // 'function' +console.log(typeof actions.arbitrary.nested.path.create.convertToFoobar) // 'function' +``` + +### Actions +Executing these functions then create the actions that you should dispatch to your Redux store. + +```js +const store = createStore(reducer) // using reducer from reduxLeaves +console.log(store.getState().counter) // 0 + +const updateCounter = actions.counter.create.update + +store.dispatch(updateCounter(5)) +console.log(store.getState().counter) // 5 + +store.dispatch(updateCounter(3)) +console.log(store.getState().counter) // 3 +``` + +### Optional `actionType` argument +Rather than directly accessing action creators from `create`, you can optionally provide an `actionType` string as an argument to `create` before accessing the action creator functions: + +```js +// Defaults, e.g. update creatorKey +console.log(typeof actions.counter.create.update) // 'function' +console.log(typeof actions.counter.create('UPDATE_COUNTER').update) // 'function' + +// Custom creatorKey of 'convertToFoobar' +console.log(typeof actions.create.convertToFoobar) // 'function' +console.log(typeof actions.create('CONVERT_TO_FOOBAR').convertToFoobar) // 'function' +``` + +Providing an `actionType` string in this way does not change the way that the reducer will respond to actions; it merely overrides the created action's `type` property to be the string passed in (which might be desirable for a debugging perspective for Redux DevTools, for example): + +```js +const createDefaultIncrement = actions.counter.create.increment +const createNamedIncrement = actions.counter.create('NAMED_INCREMENT').increment + +console.log(store.getState().counter) // 3 + +const defaultIncrement = createDefaultIncrement() +console.log(defaultIncrement.type) // 'counter/INCREMENT' +dispatch(defaultIncrement) +console.log(store.getState().counter) // 4 + +const namedIncrement = createNamedIncrement() +console.log(namedIncrement.type) // 'NAMED_INCREMENT' +dispatch(namedIncrement) +console.log(store.getState().counter) // 5 +``` + +(It is safe to override name in this way because the Redux-Leaves `reducer` does not switch over the action's `type`.) \ No newline at end of file diff --git a/docs/api/create.spec.js b/docs/api/create.spec.js new file mode 100644 index 00000000..98486709 --- /dev/null +++ b/docs/api/create.spec.js @@ -0,0 +1,59 @@ +import { createStore } from 'redux' +import reduxLeaves from '../../src'; + +describe('create has action creators keyed by default and custom creatorKeys', () => { + const initialState = { + counter: 0, + arbitrary: { + nested: { + path: ['hi!'] + } + } + } + + const reducersDict = { + convertToFoobar: () => 'foobar' + } + + const [reducer, actions] = reduxLeaves(initialState, reducersDict) + + test('All creates have default creatorKeys like update, set and push', () => { + expect(typeof actions.counter.create.update).toBe('function') + expect(typeof actions.arbitrary.nested.create.set).toBe('function') + expect(typeof actions.arbitrary.nested.path.create.push).toBe('function') + }) + + test('All creates also have supplied custom creatorKeys', () => { + expect(typeof actions.create.convertToFoobar).toBe('function') + expect(typeof actions.arbitrary.nested.path.create.convertToFoobar).toBe('function') + }) + + const store = createStore(reducer) + + test("Executing an action creator returns an action to dispatch to the Redux store", () => { + const updateCounter = actions.counter.create.update + + store.dispatch(updateCounter(5)) + expect(store.getState().counter).toBe(5) + + store.dispatch(updateCounter(3)) + expect(store.getState().counter).toBe(3) + }) + + describe('create can take a string argument as actionType', () => { + test('If given this argument, it still has properties corresponding to default and custom provided creators', () => { + expect(typeof actions.counter.create('UPDATE_COUNTER').update).toBe('function') + expect(typeof actions.create('CONVERT_TO_FOOBAR').convertToFoobar).toBe('function') + }) + + test('The created actions have the supplied actionType as type', () => { + const createNamedIncrement = actions.counter.create('NAMED_INCREMENT').increment + const namedIncrement = createNamedIncrement() + + expect(namedIncrement.type).toBe('NAMED_INCREMENT') + const currVal = store.getState().counter + store.dispatch(namedIncrement) + expect(store.getState().counter).toBe(currVal + 1) + }) + }) +}) \ No newline at end of file diff --git a/docs/creatorKeys.md b/docs/api/creatorKeys.md similarity index 86% rename from docs/creatorKeys.md rename to docs/api/creatorKeys.md index c8114e18..7b119f08 100644 --- a/docs/creatorKeys.md +++ b/docs/api/creatorKeys.md @@ -9,9 +9,9 @@ sidebar_label: Creator keys A creator key (`creatorKey`) serves two roles: -1. In a [`reducersDict`](README.md#reducersdict), it uniquely identifies a given [leaf reducer](leafReducers.md); and +1. In a [`reducersDict`](../README.md#reducersdict), it uniquely identifies a given [leaf reducer](leafReducers.md); and 2. In the `actions` API, it: - - is an action creator available at a given [leaf](leaf/README.md) through [`.create[creatorKey]`](create/defaults.md), that + - is an action creator available at a given leaf through [`.create[creatorKey]`](create.md), that - triggers the corresponding leaf reducer logic (when said action creator is called and its resultant action is dispatched). ## Example diff --git a/docs/leafReducers.md b/docs/api/leafReducers.md similarity index 91% rename from docs/leafReducers.md rename to docs/api/leafReducers.md index ba442ae0..07cfce00 100644 --- a/docs/leafReducers.md +++ b/docs/api/leafReducers.md @@ -7,11 +7,11 @@ sidebar_label: Leaf reducers # Leaf reducers -A leaf reducer is a function or configuration object that updates the state of an arbitrary [leaf](leaf/README.md) in your state tree. +A leaf reducer is a function or configuration object that updates the state of an arbitrary leaf in your state tree. They are: -- passed into [`reduxLeaves`](README.md) with a unique `creatorKey` as part of [`reducersDict`](README.md#reducersdict); and -- triggered at an arbitrary leaf only by dispatching an action created by the leaf's [`create[creatorKey]`](create/README.md) method. +- passed into [`reduxLeaves`](../README.md) with a unique [`creatorKey`](creatorKey.md) as part of [`reducersDict`](README.md#reducersdict); and +- triggered at an arbitrary leaf only by dispatching an action created by the leaf's [`create[creatorKey]`](create.md#createcreatorkey) method. ## Syntax diff --git a/docs/leafReducers.spec.js b/docs/api/leafReducers.spec.js similarity index 89% rename from docs/leafReducers.spec.js rename to docs/api/leafReducers.spec.js index 9901e8a6..fc92c435 100644 --- a/docs/leafReducers.spec.js +++ b/docs/api/leafReducers.spec.js @@ -1,5 +1,5 @@ import { createStore } from "redux"; -import reduxLeaves from '../src'; +import reduxLeaves from '../../src'; describe("Function shorthand", () => { @@ -269,37 +269,4 @@ describe("Object longhand", () => { }) }) }) - - // describe("mutate", () => { - // describe("GIVEN the leaf reducer setPropTrue", () => { - // const setPropTrue = { - // reducer: (leafState, { payload }) => { - // leafState[payload] = true - // }, - // mutate: true - // } - - // describe("WHEN we initialise reduxLeaves with empty state and setPropTrue in the dictionary", () => { - // const [reducer, actions] = reduxLeaves({}, { setPropTrue }) - // let store - // beforeEach(() => { - // store = createStore(reducer) - // }) - - // test("THEN the store's state is {}", () => { - // expect(store.getState()).toEqual({}) - // }) - - // describe("AND we dispatch actions.create.setPropTrue('foobar')", () => { - // beforeEach(() => { - // store.dispatch(actions.create.setPropTrue('foobar')) - // }) - - // test("THEN the store's state is { foobar: true }", () => { - // expect(store.getState()).toEqual({ foobar: true }) - // }) - // }) - // }) - // }) - // }) }) \ No newline at end of file diff --git a/docs/create/README.md b/docs/create/README.md deleted file mode 100644 index 1e94aa3b..00000000 --- a/docs/create/README.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -id: creators -title: Action Creators -hide_title: true -sidebar_label: Actions ---- - -# `actions` - -## Action creators - -With Redux-Leaves, all [leaf reducers](../leafReducers.md) that you define in your [`reducersDict`](../README.md#reducersdict) are automatically given a corresponding action creator, keyed by the same [`creatorKey`](../creatorKeys.md). - -`create[creatorKey]` is then an action creator function available at every single [leaf](../leaf/README.md) on our `actions` object. - -Dispatch an action created by `create[creatorKey]` will trigger the matching leaf reducer. - -## `create[creatorKey]` - -### Parameters -- `...args` *(...any)*: passed to the leaf reducer's [`argsToPayload`](../leafReducers.md#argstopayload) function - -### Returns -`action` *(object)*: a Leaf-Standard-Action - -### Example - -#### 1. Grab actions from `reduxLeaves` - -```js -import reduxLeaves from 'redux-leaves' -import { createStore } from 'redux' - -const initialState = { - foo: 1 -} - -const reducersDict = { - addOne: leafState => leafState + 1, - double: leafState => 2 * n, - exponentiate: (leafState, { payload }) => leafState ** payload -} - -const [reducer, actions] = reduxLeaves(initialState, reducersDict) -``` - -#### 2. Create actions using creator keys -```js -const { create } = actions.foo // action creators for the foo leaf of state - -const actionToAddOneToFoo = create.addOne() -const actionToDoubleFoo = create.double() -const actionToCubeFoo = create.exponentiate(3) -``` -Redux-Leaves' action creators can take optional arguments. By default, the first argument (if it exists) becomes the action's payload, but [this behaviour can be configured](../leafReducers.md#argstopayload). - -#### 3. Dispatch actions to the store -```js -const store = createStore(initialState) - -store.dispatch(actionToAddOneToFoo) -store.dispatch(actionToDoubleFoo) -store.dispatch(actionToCubeFoo) - -// ((1+1)*2)^3 = 64 -console.log(store.getState()) // { foo: 64 }; success! -``` - -## `create(actionType)` - -### Parameters -- `actionType` *(string)* - -## Example -```js -import reduxLeaves from 'redux-leaves' -import { createStore } from 'redux' - -const initialState = { - counter: 1 -} - -const [reducer, actions] = reduxLeaves(initialState, reducersDict) -const store = createStore(initialState) - -const createIncrementCounter = actions.foo.create('INCREMENT_FOO').increment -console.log(typeof createIncrementCounter) // 'function' - -const actionToIncrementCounter = createIncrementCounter() -console.log(actionToIncrementCounter.type) // 'INCREMENT_FOO' - -store.dispatch(actionToIncrementCounter) -console.log(store.getState()) // { counter: 2 } -``` - -## Defaults - -Redux-Leaves also ships with some [default action creators](defaults.md) available. \ No newline at end of file diff --git a/docs/create/apply.spec.js b/docs/create/apply.spec.js deleted file mode 100644 index aaa013e5..00000000 --- a/docs/create/apply.spec.js +++ /dev/null @@ -1,173 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../src'; - -describe("leaf.create.apply(callback): returns an action that, when dispatched, updates the leaf's state to the return value of callback(state, entireState)", () => { - - describe("Documentation example 1", () => { - describe("GIVEN setup of initialState and store as documented", () => { - const initialState = { - bool: false, - num: 2, - str: 'foo', - arr: [1, 2, 3], - obj: {} - } - - const [reducer, actions] = reduxLeaves(initialState) - let store - - beforeEach(() => { - store = createStore(reducer) - }) - - describe("WHEN we execute store.dispatch(actions.str.create.apply(state => state.toUpperCase()))", () => { - beforeEach(() => { - store.dispatch(actions.str.create.apply(state => state.toUpperCase())) - }) - - test("THEN the store's state.str is 'FOO'", () => { - expect(store.getState().str).toBe('FOO') - }) - - describe("AND we execute store.dispatch(actions.create.apply(state => ({ num: state.num, arr: state.arr })))", () => { - beforeEach(() => { - store.dispatch(actions.create.apply(state => ({ num: state.num, arr: state.arr }))) - }) - - test("THEN the store's state is { num: 2, arr: [1, 2, 3] }", () => { - expect(store.getState()).toEqual({ - num: 2, - arr: [1, 2, 3] - }) - }) - - describe("AND we execute store.dispatch(actions.arr.create.apply((leafState, entireState) => (leafState.map(element => element * entireState.num))))))", () => { - beforeEach(() => { - store.dispatch(actions.arr.create.apply((leafState, entireState) => ( - leafState.map(element => element * entireState.num) - ))) - }) - - test("THEN the store's state is { num: 2, arr: [2, 4, 6] }", () => { - expect(store.getState()).toEqual({ - num: 2, - arr: [2, 4, 6] - }) - }) - }) - }) - }) - }) - - - }) - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.counter.create.apply is a function", () => { - expect(typeof actions.counter.create.apply).toBe("function") - }) - - test("AND actions.foo.create.apply is a function", () => { - expect(typeof actions.foo.create.apply).toBe("function") - }) - - test("AND actions.nested.deep.create.apply is a function", () => { - expect(typeof actions.nested.deep.create.apply).toBe("function") - }) - - test("AND actions.nested.state.manageable.create.apply is a function", () => { - expect(typeof actions.nested.state.manageable.create.apply).toBe("function") - }) - - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.counter.create.apply(n => n * 7)", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.apply(n => n * 7)) - }) - - test("THEN actions.counter updates to 7", () => { - const state = store.getState() - expect(state.counter).toBe(7) - expect(state).toEqual({ ...initialState, counter: 7 }) - }) - }) - - describe("AND we dispatch actions.foo.create.apply(arr => ['foo', ...arr])", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.apply(arr => ['foo', ...arr])) - }) - - test("THEN actions.foo updates to ['foo', 'bar']", () => { - const state = store.getState() - expect(state.foo).toEqual(['foo', 'bar']) - expect(state).toEqual({ ...initialState, foo: ['foo', 'bar'] }) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.apply(obj => ({ ...obj, arbitrarily: true }))", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.apply(obj => ({ ...obj, arbitrarily: true }))) - }) - - test("THEN actions.nested.deep updates to { arbitrarily: true }", () => { - const state = store.getState() - expect(state.nested.deep).toEqual({ arbitrarily: true }) - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - deep: { arbitrarily: true } - } - }) - }) - }) - - describe("AND we dispatch actions.nested.state.manageable.create.apply(str => str.concat(' DEFINITELY!'))", () => { - beforeEach(() => { - store.dispatch(actions.nested.state.manageable.create.apply(str => str.concat(' DEFINITELY!'))) - }) - - test("THEN actions.nested.state.manageable updates to 'maybe...? DEFINITELY!'", () => { - const state = store.getState() - expect(state.nested.state.manageable).toEqual('maybe...? DEFINITELY!') - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - state: { - ...initialState.nested.state, - manageable: 'maybe...? DEFINITELY!' - } - } - }) - }) - }) - - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asArray/README.md b/docs/create/asArray/README.md deleted file mode 100644 index df01496e..00000000 --- a/docs/create/asArray/README.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -id: array-creators -title: Array Action Creators -hide_title: true -sidebar_label: Array ---- - -# `create.asArray` - -Every single leaf on our `actions` object has access to `create.asArray` methods. - -If the leaf was initialised with array state, then these methods are also accessible directly through the [`create` API](../defaults.md). - -If the current `leafState` is *not* an array, then it is first coerced into an array, before the state is updated according to the action dispatched. - -## Action creators -- [`.concat(array)`](#concatarray) -- [`.drop([n = 1])`](#createdropn--1) -- [`.filter(callback)`](#filter(callback)) -- [`.push(element, [index = -1], [replace = false])`](#createpushelement-index---1-replace--false) - -[Back to all `create` action creators](../defaults.md) - -## `concat(array)` -**`create.asArray.concat`** - -**alias: `create.concat`** *(when `initialLeafState` is an array)* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state by concatening it with `array`. - -### Parameters -- `array` *(array)*: the array to concatenate - -### Returns -`action` *(object)*: an object to dispatch to the `store` - -##### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: [1, 2, 3] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asArray.concat(['a', 'b', 'c'])) -console.log(store.getState().foo) // [1, 2, 3, 'a', 'b', 'c'] -``` -Back to: -* [`create.asArray` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) - -## `drop([n = 1])` -**`create.asArray.drop`** - -**alias: `create.drop`** *(when `initialLeafState` is an array)* - -Returns an object that, *when dispatched to a store created with the original state tree*, drops the first `n` elements from the leaf's state. - -### Parameters -- `n` *(number, optional)*: the number of elements to drop - -### Returns -`action` *(object)*: an object to dispatch to the `store` - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: ['a', 'b', 'c'] - bar: ['a', 'b', 'c'] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -#### No argument provided -```js -store.dispatch(actions.foo.create.asArray.drop()) -console.log(store.getState().foo) // ['b', 'c'] -``` -#### Providing an argument -```js -store.dispatch(actions.bar.create.asArray.drop(2)) -console.log(store.getState().bar) // ['c'] -``` -Back to: -* [`create.asArray` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) - -## `filter(callback)` -**via: `create.asArray.filter`** - -**alias: `create.filter`** *(when `initialLeafState` is an array)* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state by selecting elements that return true when passed to `callback`. - -(Effectively, this uses the vanilla javascript [`Array.prototype.filter(callback)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) API.) - -### Parameters -- `callback` *(function)*: the callback function to test each element with - -### Returns -`action` *(object)*: an object to dispatch to the `store` - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: [1, 2, 3, 4, 5] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asArray.filter(e => !(e % 2))) -console.log(store.getState().foo) // [2, 4] -``` -Back to: -* [`create.asArray` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) - -## `push(element, [index = -1], [replace = false])` -**`create.asArray.push`** - -**alias: `create.push`** *(when `initialLeafState` is an array)* - -Returns an object that, *when dispatched to a store created with the original state tree*, non-mutatively pushes `element` to the leaf's state at index `index`. If `replace === true`, then `element` replaces the existing element with that index. - -### Parameters -- `element` *(any)*: the element to insert to the leaf's state -- `index` *(integer, optional)*: the index of the array where `element` should be inserted -- `replace` *(boolean, optional)*: whether or not `element` should replace the current `index`th element - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: [1, 2, 3] - bar: [1, 2, 3] - foobar: [1, 2, 3] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -#### Providing element -```js -store.dispatch(actions.foo.create.asArray.push(4)) -console.log(store.getState().foo) // [1, 2, 3, 4] -``` -#### Providing element and index -```js -store.dispatch(actions.bar.create.asArray.push(4, 0)) -console.log(store.getState().bar) // [4, 1, 2, 3] -``` -#### Providing element, index and replace -```js -store.dispatch(actions.foobar.create.asArray.push(4, 0, true)) -console.log(store.getState().foobar) // [4, 2, 3] -``` -Back to: -* [`create.asArray` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) \ No newline at end of file diff --git a/docs/create/asArray/concat.spec.js b/docs/create/asArray/concat.spec.js deleted file mode 100644 index 9a24e6d9..00000000 --- a/docs/create/asArray/concat.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.concat(array): returns an action that, when dispatched, updates the leaf's state by non-mutatively concatenating it with array", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - empty: [], - integers: [1, 2, 3] - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.empty.create.concat is a function", () => { - expect(typeof actions.empty.create.concat).toBe("function") - }) - - test("AND actions.integers.create.concat is a function", () => { - expect(typeof actions.integers.create.concat).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.empty.create.concat([1, 2, 3])", () => { - beforeEach(() => { - store.dispatch(actions.empty.create.concat([1, 2, 3])) - }) - - test("THEN actions.empty state updates non-mutatively to [1, 2, 3]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - empty: [1, 2, 3] - }) - expect(initialState.empty).toEqual([]) - }) - }) - - describe("AND we dispatch actions.integers.create.concat([4, 5, 6])", () => { - beforeEach(() => { - store.dispatch(actions.integers.create.concat([4, 5, 6])) - }) - - test("THEN actions.integers state updates non-mutatively to [1, 2, 3,4, 5, 6]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - integers: [1, 2, 3, 4, 5, 6] - }) - expect(initialState.integers).toEqual([1, 2, 3]) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asArray/drop.spec.js b/docs/create/asArray/drop.spec.js deleted file mode 100644 index 1c07ba9d..00000000 --- a/docs/create/asArray/drop.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.drop(n = 1): returns an action that, when dispatched, updates the leaf's state by non-mutatively dropping the first n values", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - empty: [], - integers: [1, 2, 3] - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.empty.create.drop is a function", () => { - expect(typeof actions.empty.create.drop).toBe("function") - }) - - test("AND actions.integers.create.drop is a function", () => { - expect(typeof actions.integers.create.drop).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.integers.create.drop()", () => { - beforeEach(() => { - store.dispatch(actions.integers.create.drop()) - }) - - test("THEN actions.integers state updates non-mutatively to [2, 3]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - integers: [2, 3] - }) - expect(initialState.integers).toEqual([1, 2, 3]) - }) - }) - - describe("AND we dispatch actions.integers.create.drop(2)", () => { - beforeEach(() => { - store.dispatch(actions.integers.create.drop(2)) - }) - - test("THEN actions.integers state updates non-mutatively to [3]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - integers: [3] - }) - expect(initialState.integers).toEqual([1, 2, 3]) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asArray/filter.spec.js b/docs/create/asArray/filter.spec.js deleted file mode 100644 index 564213eb..00000000 --- a/docs/create/asArray/filter.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.filter(callback): returns an action that, when dispatched, updates the leaf's state by filtering it with the callback", () => { - - describe("Documentation example 1", () => { - - describe("GIVEN initialState, reducer, actions and created store", () => { - const initialState = { - foo: [1, 2, 3, 4, 5] - } - const [reducer, actions] = reduxLeaves(initialState) - - let store - - beforeEach(() => store = createStore(reducer)) - - test("THEN the store initialises with initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - test("AND actions.foo.create.filter is a function", () => { - expect(typeof actions.foo.create.filter).toBe("function") - }) - - describe("WHEN we dispatch foo.create.filter(e => !(e % 2))", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.filter(e => !(e % 2))) - }) - - test("THEN state.foo updates to [2, 4]", () => { - expect(store.getState()).toEqual({ foo: [2, 4] }) - }) - }) - - }) - - }) -}) \ No newline at end of file diff --git a/docs/create/asArray/push.spec.js b/docs/create/asArray/push.spec.js deleted file mode 100644 index 2f150da3..00000000 --- a/docs/create/asArray/push.spec.js +++ /dev/null @@ -1,100 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.push(element, [index = -1], [replace = false]): returns an action that, when dispatched, updates the leaf's state by non-mutatively pushing element into leaf's state at index. If replace === true, then element replaces the existing element with that index.", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - foo: [1, 2, 3], - bar: [1, 2, 3], - foobar: [1, 2, 3] - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.foo.create.push is a function", () => { - expect(typeof actions.foo.create.push).toBe("function") - }) - - test("AND actions.bar.create.push is a function", () => { - expect(typeof actions.bar.create.push).toBe("function") - }) - - test("AND actions.foobar.create.push is a function", () => { - expect(typeof actions.foobar.create.push).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.foo.create.push(4)", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.push(4)) - }) - - test("THEN actions.foo state updates non-mutatively to [1, 2, 3, 4]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - foo: [1, 2, 3, 4] - }) - expect(initialState.foo).toEqual([1, 2, 3]) - }) - }) - - describe("AND we dispatch actions.foo.create.push([4, 5])", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.push([4, 5])) - }) - - test("THEN actions.foo state updates non-mutatively to [1, 2, 3, [4, 5]]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - foo: [1, 2, 3, [4, 5]] - }) - expect(initialState.foo).toEqual([1, 2, 3]) - }) - }) - - describe("AND we dispatch actions.bar.create.push(4, 0)", () => { - beforeEach(() => { - store.dispatch(actions.bar.create.push(4, 0)) - }) - - test("THEN actions.bar state updates non-mutatively to [4, 1, 2, 3]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - bar: [4, 1, 2, 3] - }) - expect(initialState.bar).toEqual([1, 2, 3]) - }) - }) - - describe("AND we dispatch actions.foobar.create.push(4, 0, true)", () => { - beforeEach(() => { - store.dispatch(actions.foobar.create.push(4, 0, true)) - }) - - test("THEN actions.foobar state updates non-mutatively to [4, 2, 3]", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - foobar: [4, 2, 3] - }) - expect(initialState.foobar).toEqual([1, 2, 3]) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asBoolean/README.md b/docs/create/asBoolean/README.md deleted file mode 100644 index 07813663..00000000 --- a/docs/create/asBoolean/README.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -id: boolean-creators -title: Boolean Action Creators -hide_title: true -sidebar_label: Boolean ---- - -# `create.asBoolean` - -Every single leaf on our `actions` object has access to `create.asBoolean` methods. - -If the leaf was initialised with boolean state, then these methods are also accessible directly through the [`create` API](../defaults.md). - -If the current `leafState` is *not* a boolean, then it is first coerced into a boolean as `!!leafState`, before the state is updated according to the action dispatched. - -## Action creators -- [`.off()`](#off) -- [`.on()`](#on) -- [`.toggle()`](#toggle) - -[Back to all `create` action creators](../defaults.md) - -## `off()` -**`create.asBoolean.off`** - -**alias: `create.off`** *(when `initialLeafState` is a boolean)* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `false`. - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: true - bar: false -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asBoolean.off()) -console.log(store.getState().foo) // false -``` -```js -store.dispatch(actions.bar.create.asBoolean.off()) -console.log(store.getState().bar) // false -``` -Back to: -* [`create.asBoolean` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) - -## `on()` -**`create.asBoolean.on`** - -**alias: `create.on`** *(when `initialLeafState` is a boolean)* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `true`. - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: true - bar: false -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asBoolean.on()) -console.log(store.getState().foo) // true -``` -```js -store.dispatch(actions.bar.create.asBoolean.on()) -console.log(store.getState().bar) // true -``` - -## `toggle()` -**`create.asBoolean.toggle`** - -**alias: `create.toggle`** *(when `initialLeafState` is a boolean)* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `!leafState`. - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: true - bar: false -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asBoolean.toggle()) -console.log(store.getState().foo) // false -``` -```js -store.dispatch(actions.bar.create.asBoolean.toggle()) -console.log(store.getState().bar) // true -``` -Back to: -* [`create.asBoolean` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) \ No newline at end of file diff --git a/docs/create/asBoolean/off.spec.js b/docs/create/asBoolean/off.spec.js deleted file mode 100644 index 2db03c7e..00000000 --- a/docs/create/asBoolean/off.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.off(): returns an action that, when dispatched, updates the leaf's state to false", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - true: true, - false: false - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.true.create.off is a function", () => { - expect(typeof actions.true.create.off).toBe("function") - }) - - test("AND actions.false.create.off is a function", () => { - expect(typeof actions.false.create.off).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.true.create.off()", () => { - beforeEach(() => { - store.dispatch(actions.true.create.off()) - }) - - test("THEN actions.true state updates non-mutatively to false", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - true: false - }) - expect(initialState.true).toBe(true) - }) - }) - - describe("AND we dispatch actions.false.create.off()", () => { - beforeEach(() => { - store.dispatch(actions.false.create.off()) - }) - - test("THEN actions.false state remains false", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - false: false - }) - expect(initialState.false).toBe(false) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asBoolean/on.spec.js b/docs/create/asBoolean/on.spec.js deleted file mode 100644 index 074e325d..00000000 --- a/docs/create/asBoolean/on.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.on(): returns an action that, when dispatched, updates the leaf's state to true", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - true: true, - false: false - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.true.create.on is a function", () => { - expect(typeof actions.true.create.on).toBe("function") - }) - - test("AND actions.false.create.on is a function", () => { - expect(typeof actions.false.create.on).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.true.create.on()", () => { - beforeEach(() => { - store.dispatch(actions.true.create.on()) - }) - - test("THEN actions.true state remains true", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - true: true - }) - expect(initialState.true).toBe(true) - }) - }) - - describe("AND we dispatch actions.false.create.on()", () => { - beforeEach(() => { - store.dispatch(actions.false.create.on()) - }) - - test("THEN actions.false state updates non-mutatively to true", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - false: true - }) - expect(initialState.false).toBe(false) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asBoolean/toggle.spec.js b/docs/create/asBoolean/toggle.spec.js deleted file mode 100644 index 51cd73a6..00000000 --- a/docs/create/asBoolean/toggle.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.toggle(): returns an action that, when dispatched, updates the leaf's state to !state", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - true: true, - false: false - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.true.create.toggle is a function", () => { - expect(typeof actions.true.create.toggle).toBe("function") - }) - - test("AND actions.false.create.toggle is a function", () => { - expect(typeof actions.false.create.toggle).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.true.create.toggle()", () => { - beforeEach(() => { - store.dispatch(actions.true.create.toggle()) - }) - - test("THEN actions.true state updates non-mutatively to false", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - true: false - }) - expect(initialState.true).toBe(true) - }) - }) - - describe("AND we dispatch actions.false.create.toggle()", () => { - beforeEach(() => { - store.dispatch(actions.false.create.toggle()) - }) - - test("THEN actions.false state updates non-mutatively to true", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - false: true - }) - expect(initialState.false).toBe(false) - }) - }) - }) - }) - }) -}) diff --git a/docs/create/asNumber/README.md b/docs/create/asNumber/README.md deleted file mode 100644 index ed1600c8..00000000 --- a/docs/create/asNumber/README.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: number-creators -title: Number Action Creators -hide_title: true -sidebar_label: Number ---- - -# `create.asNumber` - -Every single leaf on our `actions` object has access to `create.asNumber` methods. - -If the leaf was initialised with number state, then these methods are also accessible directly through the [`create` API](../defaults.md). - -If the current `leafState` is *not* a number, then it is first coerced into an array, before the state is updated according to the action dispatched. - -## Action creators -- [`.increment([n = 1])`](#incrementn--1) - -[Back to all `create` action creators](../defaults.md) - -## `increment([n = 1])` -**`create.asNumber.increment`** - -**alias: `create.increment`** *(when `initialLeafState` is a number)* - -Returns an object that, *when dispatched to a store created with the original state tree*, increments leaf's state by `n`. - -### Parameters -- `n` *(number)*: the number to increment the leaf's state by - -### Returns -`action` *(object)*: an object to dispatch to the store - -### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: 5 - bar: 5 -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -#### No argument provided -```js -store.dispatch(actions.foo.create.asNumber.increment()) -console.log(store.getState().foo) // 6 -``` -#### Providing an argument -```js -store.dispatch(actions.bar.create.asNumber.increment(-6)) -console.log(store.getState().bar) // -1 -``` -Back to: -* [`create.asNumber` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) \ No newline at end of file diff --git a/docs/create/asNumber/increment.spec.js b/docs/create/asNumber/increment.spec.js deleted file mode 100644 index 846e06bf..00000000 --- a/docs/create/asNumber/increment.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.increment(n = 1): returns an action that, when dispatched, updates the leaf's state by non-mutatively incrementing it by n", () => { - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.counter.create.increment is a function", () => { - expect(typeof actions.counter.create.increment).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.counter.create.increment()", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.increment()) - }) - - test("THEN actions.counter state non-mutatively updates to 2", () => { - const state = store.getState() - expect(state).toEqual({ ...initialState, counter: 2 }) - expect(initialState.counter).toBe(1) - }) - }) - - describe("AND we dispatch actions.counter.create.increment(4)", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.increment(4)) - }) - - test("THEN actions.counter state non-mutatively updates to 5", () => { - const state = store.getState() - expect(state).toEqual({ ...initialState, counter: 5 }) - expect(initialState.counter).toBe(1) - }) - }) - - describe("AND we dispatch actions.counter.create.increment(-1.5)", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.increment(-1.5)) - }) - - test("THEN actions.counter state non-mutatively updates to -0.5", () => { - const state = store.getState() - expect(state).toEqual({ ...initialState, counter: -0.5 }) - expect(initialState.counter).toBe(1) - }) - }) - }) - }) - }) -}) diff --git a/docs/create/asObject/README.md b/docs/create/asObject/README.md deleted file mode 100644 index 3c2910c1..00000000 --- a/docs/create/asObject/README.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -id: object-creators -title: Object Action Creators -hide_title: true -sidebar_label: Object ---- - -# `create.asObject` - -Every single leaf on our `actions` object has access to `create.asObject` methods. - -If the leaf was initialised with [plain object](#plain-object) state, then these methods are also accessible directly through the [`create` API](../defaults.md). - -If the current `leafState` is *not* a plain object, then it is first coerced into a plain object, before the state is updated according to the action dispatched. - -#### Plain object -A plain object is created using `{}`, `new Object()` or `Object.create(null)` - -## Action creators -- [`.assign(...sources)`](#assignsources) -- [`.set(path, value)`](#setpath-value) - -[Back to all `create` action creators](../defaults.md) - -## `assign(...sources)` -**`create.asObject.assign`** - -**alias: `create.assign`** *(when `initialLeafState` is a [plain object](#plain-object))* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the copies all properties from `sources` into the leaf's state. - -(This is essentially a convenience wrapper on top of the vanilla JavaScript [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).) - -### Parameters -- `sources` *(...objects)*: the path of the property to set - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: { props: true } -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asObject.assign({ string: 'foo' })) -console.log(store.getState().foo) // { props: true, string: 'foo' } -``` -```js -store.dispatch(actions.ar.create.asObject.assign({ props: false })) -console.log(store.getState().foo) // { props: false, string: 'foo' } -``` -Back to: -* [`create.asObject` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) - - -## `set(key, value)` -**`create.asObject.set`** - -**alias: `create.set`** *(when `initialLeafState` is a [plain object](#plain-object))* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state at the property `key` with `value`. - -```js -// TODO update this -``` - -### Parameters -- `key` *(string)*: the path of the property to set - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: {} - bar: { props: true } -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asObject.set('accessed', true)) -console.log(store.getState().foo) // { accessed: true } -``` -```js -store.dispatch(actions.bar.create.asObject.set('props', false)) -console.log(store.getState().bar.props) // false -``` -Back to: -* [`create.asObject` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) \ No newline at end of file diff --git a/docs/create/asObject/assign.spec.js b/docs/create/asObject/assign.spec.js deleted file mode 100644 index 59a05dbc..00000000 --- a/docs/create/asObject/assign.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.assign(...sources): returns an action that, when dispatched, updates the leaf's state by non-mutatively copying over properties from the sources", () => { - - describe("Documentation example 1", () => { - - describe("GIVEN initial state, reducer, actions and store as in example", () => { - const initialState = { - foo: { props: true } - } - - const [reducer, actions] = reduxLeaves(initialState) - let store - - beforeEach(() => store = createStore(reducer)) - - test("THEN store initialises with initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("WHEN we dispatch actions.foo.create.assign({ string: 'foo' })", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.assign({ string: 'foo' })) - }) - - test("THEN store state.foo updates to { props: true, string: 'foo' }", () => { - expect(store.getState()).toEqual({ - foo: { props: true, string: 'foo' } - }) - }) - - }) - }) - - - }) -}) \ No newline at end of file diff --git a/docs/create/asObject/path.spec.js b/docs/create/asObject/path.spec.js deleted file mode 100644 index 714e294f..00000000 --- a/docs/create/asObject/path.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.path(path, value): returns an action that, when dispatched, updates the leaf's state by non-mutatively pathting value at state object's path", () => { - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.nested.deep.create.path is a function", () => { - expect(typeof actions.nested.deep.create.path).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.nested.deep.create.path(['arbitrarily'], true)", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.path(['arbitrarily'], true)) - }) - - test("THEN actions.counter state non-mutatively updates to { arbitrarily: true }", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - deep: { arbitrarily: true } - } - }) - expect(initialState.nested.deep).toEqual({}) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.path(['arbitrarily', 'so'], true)", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.path(['arbitrarily', 'so'], true)) - }) - - test("THEN actions.counter state non-mutatively updates to { arbitrarily: {so : true } }", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - deep: { arbitrarily: { so: true } } - } - }) - expect(initialState.nested.deep).toEqual({}) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asObject/set.spec.js b/docs/create/asObject/set.spec.js deleted file mode 100644 index 6c4f3690..00000000 --- a/docs/create/asObject/set.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.set(path, value): returns an action that, when dispatched, updates the leaf's state by non-mutatively setting value at state object's path", () => { - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.nested.deep.create.set is a function", () => { - expect(typeof actions.nested.deep.create.set).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.nested.deep.create.set('arbitrarily', true)", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.set('arbitrarily', true)) - }) - - test("THEN actions.counter state non-mutatively updates to { arbitrarily: true }", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - deep: { arbitrarily: true } - } - }) - expect(initialState.nested.deep).toEqual({}) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.set('arbitrarily.so', true)", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.set('arbitrarily.so', true)) - }) - - test("THEN actions.counter state non-mutatively updates to { 'arbitrarily.so': true }", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - deep: { 'arbitrarily.so': true } - } - }) - expect(initialState.nested.deep).toEqual({}) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/asString/README.md b/docs/create/asString/README.md deleted file mode 100644 index 60751379..00000000 --- a/docs/create/asString/README.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -id: string-creators -title: String Action Creators -hide_title: true -sidebar_label: String ---- - -# `create.asString` - -Every single leaf on our `actions` object has access to `create.asString` methods. - -If the leaf was initialised with string state, then these methods are also accessible directly through the [`create` API](../defaults.md). - -If the current `leafState` is *not* a string, then it is first coerced into a string, before the state is updated according to the action dispatched. - -## Action creators -- [`.concat(...strings)`](#concatstrings) -- [`.replace(pattern, replacement)`](#replacepattern-replacement) - -[Back to all `create` action creators](../defaults.md) - -## `replace(pattern, replacement)` -**`create.asString.replace`** - -**alias: `create.replace`** *(when `initialLeafState` is a string)* - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state by replacing some, or all, matches of `pattern` with `replacement`. - -### Parameters -- `pattern` *(RegExp | string)*: the pattern to replace - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - foo: 'foo' -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -```js -store.dispatch(actions.foo.create.asString.replace('f', 'B')) -console.log(store.getState().foo) // 'Boo' -``` -Back to: -* [`create.asString` action creators](#action-creators) -* [all `create` action creators](../README.md#action-creators) \ No newline at end of file diff --git a/docs/create/asString/replace.spec.js b/docs/create/asString/replace.spec.js deleted file mode 100644 index 0ff29050..00000000 --- a/docs/create/asString/replace.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../../src'; - -describe("leaf.create.replace(): returns an action that, when dispatched, updates the leaf's state to !state", () => { - - describe("GIVEN initialState is an object", () => { - const initialState = { - foo: "foo" - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.foo.create.replace is a function", () => { - expect(typeof actions.foo.create.replace).toBe("function") - }) - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.foo.create.replace('f', 'b')", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.replace('f', 'b')) - }) - - test("THEN actions.foo state updates non-mutatively to 'boo'", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - foo: 'boo' - }) - expect(initialState.foo).toBe('foo') - }) - }) - }) - }) - }) -}) diff --git a/docs/create/clear.spec.js b/docs/create/clear.spec.js deleted file mode 100644 index c791d0ab..00000000 --- a/docs/create/clear.spec.js +++ /dev/null @@ -1,210 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../src'; - -describe("leaf.create.clear(toNull = false): returns an action that, when dispatched, clear's the leaf's state", () => { - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.counter.create.clear is a function", () => { - expect(typeof actions.counter.create.clear).toBe("function") - }) - - test("AND actions.foo.create.clear is a function", () => { - expect(typeof actions.foo.create.clear).toBe("function") - }) - - test("AND actions.nested.deep.create.clear is a function", () => { - expect(typeof actions.nested.deep.create.clear).toBe("function") - }) - - test("AND actions.nested.state.manageable.create.clear is a function", () => { - expect(typeof actions.nested.state.manageable.create.clear).toBe("function") - }) - - - describe("AND store = createStore(reducer, otherState)", () => { - let store - const otherState = { - counter: 5, - foo: ["FOOBAR"], - nested: { - deep: { - props: true - }, - state: { - manageable: "let's find out" - } - } - } - beforeEach(() => { - store = createStore(reducer, otherState) - }) - - test("THEN store is initialised with state = otherState", () => { - expect(store.getState()).toEqual(otherState) - }) - - describe("AND we dispatch actions.counter.create.clear()", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.clear()) - }) - - test("THEN actions.counter updates to 0", () => { - const state = store.getState() - expect(state.counter).toBe(0) - expect(state).toEqual({ ...otherState, counter: 0 }) - }) - }) - - describe("AND we dispatch actions.counter.create.clear(true)", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.clear(true)) - }) - - test("THEN actions.counter updates to null", () => { - const state = store.getState() - expect(state.counter).toBeNull() - expect(state).toEqual({ ...otherState, counter: null }) - }) - }) - - describe("AND we dispatch actions.foo.create.clear()", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.clear()) - }) - - test("THEN actions.foo updates to []", () => { - const state = store.getState() - expect(state.foo).toEqual([]) - expect(state).toEqual({ ...otherState, foo: [] }) - }) - }) - - describe("AND we dispatch actions.foo.create.clear(true)", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.clear(true)) - }) - - test("THEN actions.foo updates to null", () => { - const state = store.getState() - expect(state.foo).toBeNull() - expect(state).toEqual({ ...otherState, foo: null }) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.clear()", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.clear()) - }) - - test("THEN actions.nested.deep updates to {}", () => { - const state = store.getState() - expect(state.nested.deep).toEqual({}) - expect(state).toEqual({ - ...otherState, - nested: { - ...otherState.nested, - deep: {} - } - }) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.clear(true)", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.clear(true)) - }) - - test("THEN actions.nested.deep updates to null", () => { - const state = store.getState() - expect(state.nested.deep).toBeNull() - expect(state).toEqual({ - ...otherState, - nested: { - ...otherState.nested, - deep: null - } - }) - }) - }) - - describe("AND we dispatch actions.nested.state.manageable.create.clear()", () => { - beforeEach(() => { - store.dispatch(actions.nested.state.manageable.create.clear()) - }) - - test("THEN actions.nested.state.manageable updates to ''", () => { - const state = store.getState() - expect(state.nested.state.manageable).toBe('') - expect(state).toEqual({ - ...otherState, - nested: { - ...otherState.nested, - state: { - ...otherState.nested.state, - manageable: '' - } - } - }) - }) - }) - - describe("AND we dispatch actions.nested.state.manageable.create.clear(true)", () => { - beforeEach(() => { - store.dispatch(actions.nested.state.manageable.create.clear(true)) - }) - - test("THEN actions.nested.state.manageable updates to null", () => { - const state = store.getState() - expect(state.nested.state.manageable).toBeNull() - expect(state).toEqual({ - ...otherState, - nested: { - ...otherState.nested, - state: { - ...otherState.nested.state, - manageable: null - } - } - }) - }) - }) - - describe("AND we dispatch actions.create.clear(true)", () => { - beforeEach(() => { - store.dispatch(actions.create.clear(true)) - }) - - test("THEN state updates to null", () => { - const state = store.getState() - expect(state).toBeNull() - }) - }) - - describe("AND we dispatch actions.create.clear()", () => { - beforeEach(() => { - store.dispatch(actions.create.clear()) - }) - - test("THEN state updates to {}", () => { - const state = store.getState() - expect(state).toEqual({}) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/defaults.md b/docs/create/defaults.md deleted file mode 100644 index 837c1c7b..00000000 --- a/docs/create/defaults.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -id: defaults -title: Default Action Creators -hide_title: true -sidebar_label: Universal ---- - -# `create` - -Every single [leaf](../leaf/README.md) on our [`actions`](README.md) object has a `create` property, through which we can access action creator functions corresponding to the [`reducersDict`](../README.md#reducersdict) passed to to [`reduxLeaves`](../README.md). - -In addition, `create` also contains a number of default action creators, which are listed below. - -The default action creators can be overwritten by supplying your own [leaf reducer](../leafReducers.md) definition (with the same [`creatorKey`](../creatorKeys.md)) in your `reducersDict`. - -## Action creators -### Universal -- [`.apply(callback)`](#applycallback) -- [`.clear([toNull = false])`](#cleartonull--false) -- [`.reset()`](#reset) -- [`.update(value)`](#updatevalue) - -### Type-specific -These are [spread into the `create` object](typeSpecific.md) depending on the `initialLeafState`, or accessible through their respective APIs: - -- [`create.asArray`](asArray/README.md#createasarray) -- [`create.asBoolean`](asBoolean/README.md#createasboolean) -- [`create.asNumber`](asNumber/README.md#createasnumber) -- [`create.asObject`](asObject/README.md#createasobject) -- [`create.asString`](asString/README.md#createasstring) - - -## `apply(callback)` -**`create.apply`** - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to the return value of `callback(leafState, entireState)`. - -*Note: creating an action using `apply(callback)` violates Redux's recommendation that [actions should always be serializable](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants), since the resultant action will have the function `callback` as its `payload`.* - -### Parameters -- `callback` *(function)*: invoked by the leaf's reducer with two arguments, `leafState` and `entireState` - -### Returns -`action` *(object)*: an object to dispatch to the `store` - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - bool: false, - num: 2, - str: 'foo', - arr: [1, 2, 3] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` - -Calling `create.apply` on a leaf: - -```js -store.dispatch(actions.str.create.apply(state => state.toUpperCase())) -console.log(store.getState().str) // 'FOO' -``` - -Calling `create.apply` on a branch: - -```js -store.dispatch(actions.create.apply(state => ({ num: state.num, arr: state.arr })) -console.log(store.getState()) // { num: 2, arr: [1, 2, 3] } -``` - -Calling `create.apply` with two arguments: - -```js -store.dispatch(actions.arr.create.apply( - (leafState, entireState) => leafState.map(element => element * entireState.num) -)) -console.log(store.getState()) // { num: 2, arr: [2, 4, 6] } -``` - -[Back to all `create` action creators](#action-creators) - -## `clear([toNull = false])` -**`create.clear`** - -Returns an object that, *when dispatched to a store created with the original state tree*, clears the leaf's state. - -If `toNull === true`, then it updates it to `null`, otherwise it follows the type of the leaf's initial state: -- *number*: `0` -- *string*: `''` -- *boolean*: `false` -- *array*: `[]` -- *object*: `{}` - -### Parameters -- `toNull` *(boolean, optional)*: defaults to `false` - -### Returns -`action` *(object)*: an object to dispatch to the `store` - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - bool: true, - num: 2, - str: 'foo', - arr: [1, 2, 3] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -#### bool -```js -store.dispatch(actions.bool.create.clear()) -console.log(store.getState().bool) // false - -store.dispatch(actions.bool.create.clear(true)) -console.log(store.getState().bool) // null -``` -#### num -```js -store.dispatch(actions.num.create.clear()) -console.log(store.getState().num) // 0 - -store.dispatch(actions.num.create.clear(true)) -console.log(store.getState().num) // null -``` -#### str -```js -store.dispatch(actions.str.create.clear(true)) -console.log(store.getState().str) // null - -store.dispatch(actions.str.create.clear()) -console.log(store.getState().str) // '' -``` -#### arr -```js -store.dispatch(actions.arr.create.clear(true)) -console.log(store.getState().arr) // null - -store.dispatch(actions.arr.create.clear()) -console.log(store.getState().arr) // [] -``` -#### obj -```js -store.dispatch(actions.create.clear(true)) -console.log(store.getState()) // null - -store.dispatch(actions.create.clear()) -console.log(store.getState()) // {} -``` - -[Back to all `create` action creators](#action-creators) - -## `reset()` -**`create.reset`** - -Returns an object that, *when dispatched to a store created with the original state tree*, resets the leaf's state to its initial state stored in the actions. - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - num: 2, - arr: [1, 2, 3] - -const otherState = { - num: 11, - arr: ['a', 'b', 'c'] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer, otherState) // preloads otherState - -/* store.getState() -* { -* num: 11, -* arr: ['a', 'b', 'c'] -* } -*/ - -``` -Calling `create.reset` on a leaf: -```js -store.dispatch(actions.num.create.reset()) -console.log(store.getState().num) // 2 -``` -Calling `create.reset` on a branch: -```js -store.dispatch(actions.create.reset()) -console.log(store.getState()) // { num: 2, arr: [1, 2, 3] } -``` - -[Back to all `create` action creators](#action-creators) - -## `update(value)` -**`create.update`** - -Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `value`. - -### Parameters -- `value` *(any)*: the new value for the leaf's state - -### Returns -`action` *(object)*: an object to dispatch to the store - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - bool: false, - num: 2, - str: 'foo', - arr: [1, 2, 3] -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` - -Calling `create.update` on a leaf: - -```js -store.dispatch(actions.str.create.update("I can put anything here")) -console.log(store.getState().str) // 'I can put anything here' -``` - -Calling `create.update` on a branch: -```js -store.dispatch(actions.create.update({ any: { properties: true }})) -console.log(store.getState()) // { any: { properties: true } } -``` - -[Back to all `create` action creators](#action-creators) \ No newline at end of file diff --git a/docs/create/reset.spec.js b/docs/create/reset.spec.js deleted file mode 100644 index 9b079b2a..00000000 --- a/docs/create/reset.spec.js +++ /dev/null @@ -1,176 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../src'; - -describe("leaf.create.reset(): returns an action that, when dispatched, updates the leaf's state to the reducer's initialised state", () => { - - describe("Documentation example 1", () => { - describe("GIVEN setup of initialState, otherState and store as documented", () => { - const initialState = { - num: 2, - arr: [1, 2, 3], - } - - const otherState = { - num: 11, - arr: ['a', 'b', 'c'] - } - - const [reducer, actions] = reduxLeaves(initialState) - let store - - beforeEach(() => { - store = createStore(reducer, otherState) - }) - - test("THEN store is initialised with otherState", () => { - expect(store.getState()).toEqual(otherState) - }) - - describe("WHEN we execute store.dispatch(actions.num.create.reset()))", () => { - beforeEach(() => { - store.dispatch(actions.num.create.reset()) - }) - - test("THEN the store's state.num is 2", () => { - expect(store.getState().num).toBe(2) - }) - - describe("AND we execute store.dispatch(actions.create.reset())", () => { - beforeEach(() => { - store.dispatch(actions.create.reset()) - }) - - test("THEN the store's state is { num: 2, arr: [1, 2, 3] }", () => { - expect(store.getState()).toEqual({ - num: 2, - arr: [1, 2, 3] - }) - }) - }) - }) - }) - - - }) - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.counter.create.reset is a function", () => { - expect(typeof actions.counter.create.reset).toBe("function") - }) - - test("AND actions.foo.create.reset is a function", () => { - expect(typeof actions.foo.create.reset).toBe("function") - }) - - test("AND actions.nested.deep.create.reset is a function", () => { - expect(typeof actions.nested.deep.create.reset).toBe("function") - }) - - test("AND actions.nested.state.manageable.create.reset is a function", () => { - expect(typeof actions.nested.state.manageable.create.reset).toBe("function") - }) - - - describe("AND store = createStore(reducer, otherState)", () => { - let store - const otherState = { - counter: 5, - foo: ["FOOBAR"], - nested: { - deep: { - props: true - }, - state: { - manageable: "let's find out" - } - } - } - beforeEach(() => { - store = createStore(reducer, otherState) - }) - - test("THEN store is initialised with state = otherState", () => { - expect(store.getState()).toEqual(otherState) - }) - - describe("AND we dispatch actions.counter.create.reset()", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.reset()) - }) - - test("THEN actions.counter resets to 1", () => { - const state = store.getState() - expect(state.counter).toBe(1) - expect(state).toEqual({ ...otherState, counter: 1 }) - }) - }) - - describe("AND we dispatch actions.foo.create.reset()", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.reset()) - }) - - test("THEN actions.foo resets to ['bar']", () => { - const state = store.getState() - expect(state.foo).toEqual(['bar']) - expect(state).toEqual({ ...otherState, foo: ['bar'] }) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.reset()", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.reset()) - }) - - test("THEN actions.nested.deep resets to {}", () => { - const state = store.getState() - expect(state.nested.deep).toEqual({}) - expect(state).toEqual({ - ...otherState, - nested: { - ...otherState.nested, - deep: {} - } - }) - }) - }) - - describe("AND we dispatch actions.nested.state.manageable.create.reset()", () => { - beforeEach(() => { - store.dispatch(actions.nested.state.manageable.create.reset()) - }) - - test("THEN actions.nested.state.manageable resets to 'maybe...?'", () => { - const state = store.getState() - expect(state.nested.state.manageable).toEqual('maybe...?') - expect(state).toEqual({ - ...otherState, - nested: { - ...otherState.nested, - state: { - ...otherState.nested.state, - manageable: 'maybe...?' - } - } - }) - }) - }) - - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/create/typeSpecific.md b/docs/create/typeSpecific.md deleted file mode 100644 index 9c8c6cb4..00000000 --- a/docs/create/typeSpecific.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -id: type-specific -title: Type-Specific Action Creators -hide_title: true -sidebar_label: Type-specific ---- - -# Type-specific `create` methods - -- [`create.asArray`](asArray/README.md#createasarray) -- [`create.asBoolean`](asBoolean/README.md#createasboolean) -- [`create.asNumber`](asNumber/README.md#createasnumber) -- [`create.asObject`](asObject/README.md#createasobject) -- [`create.asString`](asString/README.md#createasstring) - - -All type-agnostic methods can be accessed through every leaf's `create` property. - -Additionally, every leaf has access to type-specific methods (e.g. [`create.asArray` methods](asArray/README.md#createasarray)), even if the leaf state is not an array. - -For convenience, *if applicable at initialisation through [`reduxLeaves`](../README.md)*, type-specific methods are also aliased so that they are directly available through `create` directly. - - -#### Example -```js -import { createStore } from 'redux' -import reduxLeaves from 'reduxLeaves' - -const initialState = { - bool: false, // initialised as boolean - num: 2, // initialised as number - str: 'foo', // initialised as string - arr: [1, 2, 3], // initialised as array - obj: {} // initialised as object -} - -const [reducer, actions] = reduxLeaves(initialState) -const store = createStore(reducer) -``` -All leaves have access to [`create.asArray.push`](asArray/README.md#createpushelement-index---1-replace--false): -```js -console.log(typeof actions.bool.create.asArray.push) // function -console.log(typeof actions.num.create.asArray.push) // function -console.log(typeof actions.str.create.asArray.push) // function -console.log(typeof actions.str.arr.create.asArray.push) // function -console.log(typeof actions.str.obj.create.asArray.push) // function -``` -But **only** `actions.arr.create` has *direct* access to `create.push`, since it is the only leaf that was initialised as an array: -```js -console.log(typeof actions.bool.create.push) // undefined -console.log(typeof actions.num.create.push) // undefined -console.log(typeof actions.str.create.push) // undefined -console.log(typeof actions.str.arr.create.push) // function: initialised as array -console.log(typeof actions.str.obj.create.push) // undefined -``` \ No newline at end of file diff --git a/docs/create/update.spec.js b/docs/create/update.spec.js deleted file mode 100644 index f4b88440..00000000 --- a/docs/create/update.spec.js +++ /dev/null @@ -1,174 +0,0 @@ -import { createStore } from "redux"; -import reduxLeaves from '../../src'; - -describe("leaf.create.update(value): returns an action that, when dispatched, updates the leaf's state to value", () => { - - describe("Documentation example 1", () => { - describe("GIVEN setup of initialState and store as documented", () => { - const initialState = { - bool: false, - num: 2, - str: 'foo', - arr: [1, 2, 3] - } - - const [reducer, actions] = reduxLeaves(initialState) - let store - - beforeEach(() => { - store = createStore(reducer) - }) - - describe("WHEN we execute store.dispatch(actions.str.create.update('I can put anything here'))", () => { - beforeEach(() => { - store.dispatch(actions.str.create.update('I can put anything here')) - }) - - test("THEN the store's state.str is 'I can put anything here'", () => { - expect(store.getState().str).toBe('I can put anything here') - }) - - describe("AND we execute store.dispatch(actions.create.update({ any: { properties: true } }))", () => { - beforeEach(() => { - store.dispatch(actions.create.update({ any: { properties: true } })) - }) - - test("THEN the store's state is { any: { properties: { true }} }", () => { - expect(store.getState()).toEqual({ - any: { properties: true } - }) - }) - }) - }) - - describe("WHEN we create an action with actions.str.create('DID_AN_UPDATE').update('I can put anything here')", () => { - const action = actions.str.create('DID_AN_UPDATE').update('I can put anything here') - - test("THEN the action's type is 'DID_AN_UPDATE'", () => { - expect(action.type).toBe('DID_AN_UPDATE') - }) - - describe('WHEN we dispatch that action to the store', () => { - beforeEach(() => { - store.dispatch(actions.str.create('DID_AN_UPDATE').update('I can put anything here')) - }) - - test("THEN the store's state.str is 'I can put anything here'", () => { - expect(store.getState().str).toBe('I can put anything here') - }) - }) - }) - }) - - - }) - - describe("GIVEN non-trivially nested API (as in the documentation)", () => { - const initialState = { - counter: 1, - foo: ["bar"], - nested: { - deep: {}, - state: { - manageable: "maybe...?" - } - } - } - - describe("WHEN [reducer, actions] = reduxLeaves(initialState)", () => { - const [reducer, actions] = reduxLeaves(initialState) - - test("THEN actions.counter.create.update is a function", () => { - expect(typeof actions.counter.create.update).toBe("function") - }) - - test("AND actions.foo.create.update is a function", () => { - expect(typeof actions.foo.create.update).toBe("function") - }) - - test("AND actions.nested.deep.create.update is a function", () => { - expect(typeof actions.nested.deep.create.update).toBe("function") - }) - - test("AND actions.nested.state.manageable.create.update is a function", () => { - expect(typeof actions.nested.state.manageable.create.update).toBe("function") - }) - - - describe("AND store = createStore(reducer)", () => { - let store - beforeEach(() => { - store = createStore(reducer) - }) - - test("THEN store is initialised with state = initialState", () => { - expect(store.getState()).toEqual(initialState) - }) - - describe("AND we dispatch actions.counter.create.update(9)", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.update(9)) - }) - - test("THEN actions.counter state updates non-mutatively to 9", () => { - const state = store.getState() - expect(state).toEqual({ ...initialState, counter: 9 }) - expect(initialState.counter).toBe(1) - }) - }) - - describe("AND we dispatch actions.foo.create.update(['f', 'o', 'o'])", () => { - beforeEach(() => { - store.dispatch(actions.foo.create.update(['f', 'o', 'o'])) - }) - - test("THEN actions.foo state updates non-mutatively to ['f', 'o', 'o']", () => { - const state = store.getState() - expect(state).toEqual({ ...initialState, foo: ['f', 'o', 'o'] }) - expect(initialState.foo).toEqual(['bar']) - }) - }) - - describe("AND we dispatch actions.nested.deep.create.update({ here: true })", () => { - beforeEach(() => { - store.dispatch(actions.nested.deep.create.update({ here: true })) - }) - - test("THEN actions.nested.deep updates to { here: true }", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - deep: { here: true } - } - }) - expect(initialState.nested.deep).toEqual({}) - }) - }) - - describe("AND we dispatch actions.nested.state.manageabl.createe.update('thanks to redux-leaves!')", () => { - beforeEach(() => { - store.dispatch(actions.nested.state.manageable.create.update('thanks to redux-leaves!')) - }) - - test("THEN actions.nested.state.manageable updates to 'thanks to redux-leaves!'", () => { - const state = store.getState() - expect(state).toEqual({ - ...initialState, - nested: { - ...initialState.nested, - state: { - ...initialState.nested.state, - manageable: 'thanks to redux-leaves!' - } - } - }) - expect(initialState.nested.state.manageable).toBe("maybe...?") - }) - }) - - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/defaults/README.md b/docs/defaults/README.md new file mode 100644 index 00000000..fd4f7a1e --- /dev/null +++ b/docs/defaults/README.md @@ -0,0 +1,41 @@ +--- +id: +title: Default action creators by type +hide_title: true +sidebar_label: By type +--- + +# Default action creators by type + +All of the below action creators are availble through the [`create`](../api/create.md) API at any arbitrary leaf of [`actions`](../api/actions.md). + +Some are more useful depending on the type of leaf state you are operating with: + +## any +- [`.apply(callback)`](apply.md) +- [`.clear([toNull = false])`](clear.md) +- [`.reset()`](reset.md) +- [`.update(value)`](update.md) + +## array +- [`.concat(array)`](concat.md) +- [`.drop([n = 1])`](drop.md) +- [`.filter(callback)`](filter.md) +- [`.push(element, [index = -1], [replace = false])`](push.md) + +## boolean +- [`.off()`](off.md) +- [`.on()`](on.md) +- [`.toggle()`](toggle.md) + +# number +- [`.increment([n = 1])`](increment.md) + +## object +- [`.assign(...sources)`](assign.md) +- [`.path(path, value)`](path.md) +- [`.set(key, value)`](set.md) + +## string +- [`.concat(string)`](concat.md) + diff --git a/docs/defaults/apply.md b/docs/defaults/apply.md new file mode 100644 index 00000000..4c6933c3 --- /dev/null +++ b/docs/defaults/apply.md @@ -0,0 +1,71 @@ +--- +id: apply +title: apply +hide_title: true +sidebar_label: apply +--- + +# `apply(callback)` +**`create.apply`** +**`create(actionType).apply`** +*Appropriate leaf state: any* + +Returns an action object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to the return value of `callback(leafState, treeState)`. + +*Note: creating an action using `apply(callback)` does not follow Redux's non-enforced recommendation that [actions should always be serializable](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants), since the resultant action will have the function `callback` as its `payload`.* + +## Parameters +- `callback` *(function)*: invoked by the leaf's reducer with two arguments, `leafState` and `entireState` + +## Returns +`action` *(object)*: an object to dispatch to the `store` + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + bool: false, + num: 2, + str: 'foo', + arr: [1, 2, 3] +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Calling `create.apply` on a leaf: + +```js +const applyToString = actions.str.create.apply +store.dispatch(applyToString(state => state.toUpperCase())) +console.log(store.getState().str) // 'FOO' +``` + +### Calling `create(actionType).apply` on a leaf: + +```js +const applyToBoolean = actions.bool.create('APPLY_TO_BOOLEAN').apply +store.dispatch(applyToBoolean(state => !state)) +console.log(store.getState().bool) // true +``` + +### Calling `create.apply` on a branch: + +```js +const applyToState = actions.create.apply +store.dispatch(applyToState(state => ({ num: state.num, arr: state.arr })) +console.log(store.getState()) // { num: 2, arr: [1, 2, 3] } +``` + +### Calling `create.apply` with two arguments: + +```js +const applyToArray = actions.arr.create.apply +store.dispatch(applyToArray( + (leafState, treeState) => leafState.map(element => element * treeState.num) +)) +console.log(store.getState()) // { num: 2, arr: [2, 4, 6] } +``` \ No newline at end of file diff --git a/docs/defaults/apply.spec.js b/docs/defaults/apply.spec.js new file mode 100644 index 00000000..e285d94f --- /dev/null +++ b/docs/defaults/apply.spec.js @@ -0,0 +1,41 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.apply(callback): returns an action that, when dispatched, updates the leaf's state to the return value of callback(state, entireState)", () => { + const initialState = { + bool: false, + num: 2, + str: 'foo', + arr: [1, 2, 3], + obj: {} + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("Calling create.apply on a leaf", () => { + const applyToString = actions.str.create.apply + store.dispatch(applyToString(state => state.toUpperCase())) + expect(store.getState().str).toBe('FOO') + }) + + test("Calling create(actionType).apply on a leaf", () => { + const applyToBoolean = actions.bool.create('APPLY_TO_BOOLEAN').apply + store.dispatch(applyToBoolean(state => !state)) + expect(store.getState().bool).toBe(true) + }) + + test("Calling create.apply on a branch", () => { + const applyToState = actions.create.apply + store.dispatch(applyToState(state => ({ num: state.num, arr: state.arr }))) + expect(store.getState()).toEqual({ num: 2, arr: [1, 2, 3] }) + }) + + test("Calling create.apply with two arguments", () => { + const applyToArray = actions.arr.create.apply + store.dispatch(applyToArray( + (leafState, treeState) => leafState.map(element => element * treeState.num) + )) + expect(store.getState()).toEqual({ num: 2, arr: [2, 4, 6] }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/assign.md b/docs/defaults/assign.md new file mode 100644 index 00000000..726be3f3 --- /dev/null +++ b/docs/defaults/assign.md @@ -0,0 +1,47 @@ +--- +id: assign +title: assign +hide_title: true +sidebar_label: assign +--- + +# `assign(...sources)` +**`create.assign`** +**`create(actionType).assign`** + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the copies all properties from `sources` into the leaf's state. + +(This is essentially a convenience wrapper on top of the vanilla JavaScript [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).) + +## Parameters +- `sources` *(...objects)*: the path of the property to set + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: { props: true }, + bar: { props: false } +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Assigning new properties +```js +const assignToFoo = actions.foo.create.assign +store.dispatch(assignToFoo({ count: 2 })) +console.log(store.getState().foo) // { props: true, count: 2 } +``` +### Overwriting properties +```js +const assignToBar = actions.bar.create.assign +store.dispatch(assignToBar({ props: true })) +console.log(store.getState().bar) // { props: true } +``` \ No newline at end of file diff --git a/docs/defaults/assign.spec.js b/docs/defaults/assign.spec.js new file mode 100644 index 00000000..1ba8aab2 --- /dev/null +++ b/docs/defaults/assign.spec.js @@ -0,0 +1,24 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.assign(...sources): returns an action that, when dispatched, updates the leaf's state by non-mutatively copying over properties from the sources", () => { + const initialState = { + foo: { props: true }, + bar: { props: false } + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("Assigning new properties", () => { + const assignToFoo = actions.foo.create.assign + store.dispatch(assignToFoo({ count: 2 })) + expect(store.getState().foo).toEqual({ props: true, count: 2 }) + }) + + test("Overwriting properties", () => { + const assignToBar = actions.bar.create.assign + store.dispatch(assignToBar({ props: true })) + expect(store.getState().bar).toEqual({ props: true }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/clear.md b/docs/defaults/clear.md new file mode 100644 index 00000000..f93cd319 --- /dev/null +++ b/docs/defaults/clear.md @@ -0,0 +1,92 @@ +--- +id: clear +title: clear +hide_title: true +sidebar_label: clear +--- + +# `clear(toNull = false)` +**create.clear** +**create(actionType).clear** +*Appropriate leaf state: any* + +Returns an object that, *when dispatched to a store created with the original state tree*, clears the leaf's state. + +If `toNull === true`, then it updates it to `null`, otherwise it follows the type of the leaf's state: +- *number* clears to `0` +- *string* clears to `''` +- *boolean* clears to `false` +- *array* clears to `[]` +- *object* clears to `{}` + +## Parameters +- `toNull` *(boolean, optional)*: defaults to `false` + +## Returns +`action` *(object)*: an object to dispatch to the `store` + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + bool: true, + num: 2, + str: 'foo', + arr: [1, 2, 3] +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` +### Boolean state +```js +const clearBool = actions.bool.create.clear + +store.dispatch(clearBool()) +console.log(store.getState().bool) // false + +store.dispatch(clearBool(true)) +console.log(store.getState().bool) // null +``` +### Number state +```js +const clearNum = actions.num.create.clear + +store.dispatch(clearNum()) +console.log(store.getState().num) // 0 + +store.dispatch(clearNum(true)) +console.log(store.getState().num) // null +``` +### String state +```js +const clearStr = actions.str.create.clear + +store.dispatch(clearStr()) +console.log(store.getState().str) // '' + +store.dispatch(clearStr(true)) +console.log(store.getState().str) // null +``` +### Array state +```js +const clearArr = actions.arr.create.clear + +store.dispatch(clearArr()) +console.log(store.getState().arr) // [] + +store.dispatch(clearArr(true)) +console.log(store.getState().arr) // null +``` +### Object state +```js +const clearState = actions.create.clear + +store.dispatch(clearState()) +console.log(store.getState()) // {} + +store.dispatch(clearState(true)) +console.log(store.getState()) // null +``` \ No newline at end of file diff --git a/docs/defaults/clear.spec.js b/docs/defaults/clear.spec.js new file mode 100644 index 00000000..5fc2bf87 --- /dev/null +++ b/docs/defaults/clear.spec.js @@ -0,0 +1,84 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.clear(toNull = false): returns an action that, when dispatched, clear's the leaf's state", () => { + const initialState = { + bool: true, + num: 2, + str: 'foo', + arr: [1, 2, 3] + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + describe('Boolean state', () => { + const clearBool = actions.bool.create.clear + + it('Clears to false', () => { + store.dispatch(clearBool()) + expect(store.getState().bool).toBe(false) + }) + + it('Clears to null if passed true', () => { + store.dispatch(clearBool(true)) + expect(store.getState().bool).toBe(null) + }) + }) + + describe('Number state', () => { + const clearNum = actions.num.create.clear + + it('Clears to 0', () => { + store.dispatch(clearNum()) + expect(store.getState().num).toBe(0) + }) + + it('Clears to null if passed true', () => { + store.dispatch(clearNum(true)) + expect(store.getState().num).toBe(null) + }) + }) + + describe('String state', () => { + const clearStr = actions.str.create.clear + + it("Clears to ''", () => { + store.dispatch(clearStr()) + expect(store.getState().str).toBe('') + }) + + it('Clears to null if passed true', () => { + store.dispatch(clearStr(true)) + expect(store.getState().str).toBe(null) + }) + }) + + describe('Array state', () => { + const clearArr = actions.arr.create.clear + + it("Clears to []", () => { + store.dispatch(clearArr()) + expect(store.getState().arr).toEqual([]) + }) + + it('Clears to null if passed true', () => { + store.dispatch(clearArr(true)) + expect(store.getState().arr).toBe(null) + }) + }) + + describe('Object state', () => { + const clearState = actions.create.clear + + it("Clears to []", () => { + store.dispatch(clearState()) + expect(store.getState()).toEqual({}) + }) + + it('Clears to null if passed true', () => { + store.dispatch(clearState(true)) + expect(store.getState()).toBe(null) + }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/concat.md b/docs/defaults/concat.md new file mode 100644 index 00000000..ca656ecc --- /dev/null +++ b/docs/defaults/concat.md @@ -0,0 +1,47 @@ +--- +id: concat +title: concat +hide_title: true +sidebar_label: concat +--- + +# `concat(arrayOrString)` +**`create.concat`** +**`create(actionType).concat`** +*Appropriate leaf state: array | string* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state by concatening it with `arrayOrString`. + +## Parameters +- `arrayOrString` *(array | string)*: the array to concatenate + +## Returns +`action` *(object)*: an object to dispatch to the `store` + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + arr: [1, 2, 3], + str: 'foo' +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Concatenating an array +```js +const concatToArr = actions.arr.create.concat +store.dispatch(concatToArr(['a', 'b', 'c'])) +console.log(store.getState().arr) // [1, 2, 3, 'a', 'b', 'c'] +``` + +### Concatenating a string +```js +const concatToStr = actions.str.create.concat +store.dispatch(concatToStr('bar')) +console.log(store.getState().str) // 'foobar' +``` diff --git a/docs/defaults/concat.spec.js b/docs/defaults/concat.spec.js new file mode 100644 index 00000000..4e46c6f3 --- /dev/null +++ b/docs/defaults/concat.spec.js @@ -0,0 +1,24 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.concat(array): returns an action that, when dispatched, updates the leaf's state by non-mutatively concatenating it with array", () => { + const initialState = { + arr: [1, 2, 3], + str: 'foo' + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test('Concatenating an array', () => { + const concatToArr = actions.arr.create.concat + store.dispatch(concatToArr(['a', 'b', 'c'])) + expect(store.getState().arr).toEqual([1, 2, 3, 'a', 'b', 'c']) + }) + + test('Concatenating a string', () => { + const concatToStr = actions.str.create.concat + store.dispatch(concatToStr('bar')) + expect(store.getState().str).toBe('foobar') + }) +}) \ No newline at end of file diff --git a/docs/defaults/drop.md b/docs/defaults/drop.md new file mode 100644 index 00000000..080a8d86 --- /dev/null +++ b/docs/defaults/drop.md @@ -0,0 +1,46 @@ +--- +id: drop +title: drop +hide_title: true +sidebar_label: drop +--- + +# `drop([n = 1])` +**`create.drop`** +**`create(actionType).drop** +*Appropriate leaf state: array* + +Returns an object that, *when dispatched to a store created with the original state tree*, drops the first `n` elements from the leaf's state. + +## Parameters +- `n` *(number, optional)*: the number of elements to drop + +## Returns +`action` *(object)*: an object to dispatch to the `store` + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: ['a', 'b', 'c'], + bar: ['a', 'b', 'c'] +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` +### No argument provided +```js +const dropFromFoo = actions.foo.create.drop +store.dispatch(dropFromFoo()) +console.log(store.getState().foo) // ['b', 'c'] +``` + +### Providing an argument +```js +const dropFromBar = actions.bar.create.drop +store.dispatch(dropFromBar(2)) +console.log(store.getState().bar) // ['c'] +``` \ No newline at end of file diff --git a/docs/defaults/drop.spec.js b/docs/defaults/drop.spec.js new file mode 100644 index 00000000..ca3776bb --- /dev/null +++ b/docs/defaults/drop.spec.js @@ -0,0 +1,24 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.drop(n = 1): returns an action that, when dispatched, updates the leaf's state by non-mutatively dropping the first n values", () => { + const initialState = { + foo: ['a', 'b', 'c'], + bar: ['a', 'b', 'c'] + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("No argument provided", () => { + const dropFromFoo = actions.foo.create.drop + store.dispatch(dropFromFoo()) + expect(store.getState().foo).toEqual(['b', 'c']) + }) + + test("Providing an argument", () => { + const dropFromBar = actions.bar.create.drop + store.dispatch(dropFromBar(2)) + expect(store.getState().bar).toEqual(['c']) + }) +}) \ No newline at end of file diff --git a/docs/defaults/filter.md b/docs/defaults/filter.md new file mode 100644 index 00000000..d991d60b --- /dev/null +++ b/docs/defaults/filter.md @@ -0,0 +1,49 @@ +--- +id: filter +title: filter +hide_title: true +sidebar_label: filter +--- + +# `filter(callback)` +**`create.filter`** +**`create(actionType).filter`** +*Appropriate leaf state: array* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state by selecting elements that return true when passed to `callback`. + +(Effectively, this uses the vanilla javascript [`Array.prototype.filter(callback)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) API.) + +## Parameters +- `callback` *(function)*: the callback function to test each element with + +## Returns +`action` *(object)*: an object to dispatch to the `store` + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: [1, 2, 3, 4, 5], + bar: ['cat', 'dog', 'bat'] +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Calling create.filter +```js +const filterFoo = actions.foo.create.filter +store.dispatch(filterFoo(e => !(e % 2))) +console.log(store.getState().foo) // [2, 4] +``` + +### Calling create(actionType).filter +```js +const filterBar = actions.bar.create('FILTER_BAR').filter +store.dispatch(filterBar(e => e.includes('at'))) +console.log(store.getState().bar) // ['cat', 'bat'] +``` \ No newline at end of file diff --git a/docs/defaults/filter.spec.js b/docs/defaults/filter.spec.js new file mode 100644 index 00000000..1c782088 --- /dev/null +++ b/docs/defaults/filter.spec.js @@ -0,0 +1,26 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.filter(callback): returns an action that, when dispatched, updates the leaf's state by filtering it with the callback", () => { + const initialState = { + foo: [1, 2, 3, 4, 5], + bar: ['cat', 'dog', 'bat'] + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test('Calling create.filter', () => { + const filterFoo = actions.foo.create.filter + store.dispatch(filterFoo(e => !(e % 2))) + expect(store.getState().foo).toEqual([2, 4]) + }) + + test('Calling create(actionType).filter', () => { + const filterBar = actions.bar.create('FILTER_BAR').filter + store.dispatch(filterBar(e => e.includes('at'))) + expect(store.getState().bar).toEqual(['cat', 'bat']) + }) + + +}) \ No newline at end of file diff --git a/docs/defaults/increment.md b/docs/defaults/increment.md new file mode 100644 index 00000000..c72169c3 --- /dev/null +++ b/docs/defaults/increment.md @@ -0,0 +1,45 @@ +--- +id: increment +title: increment +hide_title: true +sidebar_label: increment +--- + +# `increment([n = 1])` +**`create.increment`** +**`create(actionType).increment`** +*Appropriate leaf state: number* + +Returns an object that, *when dispatched to a store created with the original state tree*, increments leaf's state by `n`. + +## Parameters +- `n` *(number, optional)*: the number to increment the leaf's state by, defaulting to 1 + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: 5, + bar: 5 +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` +### No argument provided +```js +const incrementFoo = actions.foo.create.increment +store.dispatch(incrementFoo()) +console.log(store.getState().foo) // 6 +``` +### Providing an argument +```js +const incrementBar = actions.bar.create.increment +store.dispatch(incrementBar(37)) +console.log(store.getState().bar) // 42 +``` \ No newline at end of file diff --git a/docs/defaults/increment.spec.js b/docs/defaults/increment.spec.js new file mode 100644 index 00000000..111cea88 --- /dev/null +++ b/docs/defaults/increment.spec.js @@ -0,0 +1,24 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.increment(n = 1): returns an action that, when dispatched, updates the leaf's state by non-mutatively incrementing it by n", () => { + const initialState = { + foo: 5, + bar: 5 + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("No argument provided", () => { + const incrementFoo = actions.foo.create.increment + store.dispatch(incrementFoo()) + expect(store.getState().foo).toBe(6) + }) + + test("Providing an argument", () => { + const incrementBar = actions.bar.create.increment + store.dispatch(incrementBar(37)) + expect(store.getState().bar).toBe(42) + }) +}) diff --git a/docs/defaults/off.md b/docs/defaults/off.md new file mode 100644 index 00000000..8783c6fe --- /dev/null +++ b/docs/defaults/off.md @@ -0,0 +1,44 @@ +--- +id: off +title: off +hide_title: true +sidebar_label: off +--- + +# `off()` +**`create.off`** +**`create(actionType).off`** +*Appropriate leaf state: boolean* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `false`. + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: true, + bar: true +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Calling `create.off` +```js +const turnOffFoo = actions.foo.create.off +store.dispatch(turnOffFoo()) +console.log(store.getState().foo) // false +``` + +### Calling `create(actionType).off` +```js +const turnOffBar = actions.bar.create('TURN_OFF_BAR').off +store.dispatch(turnOffBar()) +console.log(store.getState().bar) // false +``` \ No newline at end of file diff --git a/docs/defaults/off.spec.js b/docs/defaults/off.spec.js new file mode 100644 index 00000000..7e6e99e1 --- /dev/null +++ b/docs/defaults/off.spec.js @@ -0,0 +1,27 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.off(): returns an action that, when dispatched, updates the leaf's state to false", () => { + + describe("GIVEN initialState is an object", () => { + const initialState = { + foo: true, + bar: true + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test('Calling create.off', () => { + const turnOffFoo = actions.foo.create.off + store.dispatch(turnOffFoo()) + expect(store.getState().foo).toBe(false) + }) + + test('Calling create(actionType).off', () => { + const turnOffBar = actions.bar.create("TURN_OFF_BAR").off + store.dispatch(turnOffBar()) + expect(store.getState().bar).toBe(false) + }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/on.md b/docs/defaults/on.md new file mode 100644 index 00000000..6eeece28 --- /dev/null +++ b/docs/defaults/on.md @@ -0,0 +1,44 @@ +--- +id: on +title: on +hide_title: true +sidebar_label: on +--- + +# `on()` +**`create.on`** +**`create(actionType).on`** +*Appropriate leaf state: boolean* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `true`. + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: false, + bar: false +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Calling `create.on` +```js +const turnOffFoo = actions.foo.create.on +store.dispatch(turnOffFoo()) +console.log(store.getState().foo) // true +``` + +### Calling `create(actionType).on` +```js +const turnOffBar = actions.bar.create('TURN_ON_BAR').on +store.dispatch(turnOffBar()) +console.log(store.getState().bar) // ture +``` \ No newline at end of file diff --git a/docs/defaults/on.spec.js b/docs/defaults/on.spec.js new file mode 100644 index 00000000..1bfffee3 --- /dev/null +++ b/docs/defaults/on.spec.js @@ -0,0 +1,27 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.createon(): returns an action that, when dispatched, updates the leaf's state to false", () => { + + describe("GIVEN initialState is an object", () => { + const initialState = { + foo: false, + bar: false + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test('Calling create.on', () => { + const turnOnFoo = actions.foo.create.on + store.dispatch(turnOnFoo()) + expect(store.getState().foo).toBe(true) + }) + + test('Calling create(actionType).on', () => { + const turnOnBar = actions.bar.create("TURN_ON_BAR").on + store.dispatch(turnOnBar()) + expect(store.getState().bar).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/path.md b/docs/defaults/path.md new file mode 100644 index 00000000..df98dc62 --- /dev/null +++ b/docs/defaults/path.md @@ -0,0 +1,48 @@ +--- +id: path +title: path +hide_title: true +sidebar_label: path +--- + +# `path(path, value)` +**`create.path`** +**`create(actionType).path`** +*Appropriate leaf type: object* + +Returns an object that, *when dispatched to a store created with the original state tree*, sets a property at `path` as `value`. + +## Parameters +- `path` *(string[])*: the property path to set at +- `value` *(any)*: the value to set + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: {} + bar: { arbitrary: { keys: 3 } } +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Setting a new property +```js +const setAtPathInFoo = actions.foo.create.path +store.dispatch(setAtPathInFoo(['nested', 'deep'], true)) +console.log(store.getState().foo) // { nested: { deep: true } } + +``` +### Overwriting a property +```js +const setAtPathInBar = actions.bar.create("SET_AT_PATH_IN_BAR").path +store.dispatch(setAtPathInBar(['arbitrary', 'keys'], 5)) +console.log(store.getState().bar) // { arbitrary: { keys: 5 } } +``` \ No newline at end of file diff --git a/docs/defaults/path.spec.js b/docs/defaults/path.spec.js new file mode 100644 index 00000000..a5a27626 --- /dev/null +++ b/docs/defaults/path.spec.js @@ -0,0 +1,24 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.path(path, value): returns an action that, when dispatched, updates the leaf's state by setting a proprety at path as value", () => { + const initialState = { + foo: {}, + bar: {} + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("Setting a new property", () => { + const setAtPathInFoo = actions.foo.create.path + store.dispatch(setAtPathInFoo(['nested', 'deep'], true)) + expect(store.getState().foo).toEqual({ nested: { deep: true } }) + }) + + test("Overwriting a property", () => { + const setAtPathInBar = actions.bar.create.path + store.dispatch(setAtPathInBar(['arbitrary', 'keys'], 5)) + expect(store.getState().bar).toEqual({ arbitrary: { keys: 5 }}) + }) +}) \ No newline at end of file diff --git a/docs/defaults/push.md b/docs/defaults/push.md new file mode 100644 index 00000000..da8400cb --- /dev/null +++ b/docs/defaults/push.md @@ -0,0 +1,54 @@ +--- +id: push +title: push +hide_title: true +sidebar_label: push +--- + +# `push(element, [index = -1], [replace = false])` +**`create.push`** +**`create(actionType).push`** +*Appropriate leaf type: array* + +Returns an object that, *when dispatched to a store created with the original state tree*, non-mutatively pushes `element` to the leaf's state at index `index`. If `replace` is `true`, then `element` replaces the existing element with that index. + +## Parameters +- `element` *(any)*: the element to insert to the leaf's state +- `index` *(integer, optional)*: the index of the array where `element` should be inserted +- `replace` *(boolean, optional)*: whether or not `element` should replace the current `index`th element + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: [1, 2, 3], + bar: [1, 2, 3], + foobar: [1, 2, 3] +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` +### Providing element +```js +const pushToFoo = actions.foo.create.push +store.dispatch(pushToFoo(4)) +console.log(store.getState().foo) // [1, 2, 3, 4] +``` +### Providing element and index +```js +const pushToBar = actions.bar.create.push +store.dispatch(pushToBar(4, 0)) // push 4 to have index 0 +console.log(store.getState().bar) // [4, 1, 2, 3] +``` +### Providing element, index and replace +```js +const pushToFoobar = actions.foobar.create.push +store.dispatch(pushToFoobar(4, 0, true)) // replace 0th element with 4 +console.log(store.getState().foobar) // [4, 2, 3] +``` \ No newline at end of file diff --git a/docs/defaults/push.spec.js b/docs/defaults/push.spec.js new file mode 100644 index 00000000..1396ea4f --- /dev/null +++ b/docs/defaults/push.spec.js @@ -0,0 +1,31 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.push(element, [index = -1], [replace = false]): returns an action that, when dispatched, updates the leaf's state by non-mutatively pushing element into leaf's state at index. If replace === true, then element replaces the existing element with that index.", () => { + const initialState = { + foo: [1, 2, 3], + bar: [1, 2, 3], + foobar: [1, 2, 3] + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("Providing element", () => { + const pushToFoo = actions.foo.create.push + store.dispatch(pushToFoo(4)) + expect(store.getState().foo).toEqual([1, 2, 3, 4]) + }) + + test("Providing element and index", () => { + const pushToBar = actions.bar.create.push + store.dispatch(pushToBar(4, 0)) + expect(store.getState().bar).toEqual([4, 1, 2, 3]) + }) + + test("Providing element, index and replace", () => { + const pushToFoobar = actions.foobar.create.push + store.dispatch(pushToFoobar(4, 0, true)) + expect(store.getState().foobar).toEqual([4, 2, 3]) + }) +}) \ No newline at end of file diff --git a/docs/defaults/reset.md b/docs/defaults/reset.md new file mode 100644 index 00000000..66c5e75d --- /dev/null +++ b/docs/defaults/reset.md @@ -0,0 +1,66 @@ +--- +id: reset +title: reset +hide_title: true +sidebar_label: reset +--- + +# `reset()` +**`create.reset`** +**`create(actionType).reset`** +*Appropriate leaf state: any* + +Returns an object that, *when dispatched to a store created with the original state tree*, resets the leaf's state to its initial state stored in the actions. + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + num: 2, + arr: [1, 2, 3], + bool: true +} + +const otherState = { + num: 11, + arr: ['a', 'b', 'c'], + bool: false +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer, otherState) // preloads otherState + +/* store.getState() +* { +* num: 11, +* arr: ['a', 'b', 'c'] +* } +*/ + +``` + +### Calling `create.reset` on a leaf: +```js +const resetNum = actions.num.create.reset +store.dispatch(resetNum()) +console.log(store.getState().num) // 2 +``` + +### Calling `create(actionType).reset` on a leaf: +```js +const resetBool = actions.bool.create.reset +store.dispatch(resetBool()) +console.log(store.getState().bool) // true +``` + +### Calling `create.reset` on a branch: +```js +const resetState = actions.create.reset +store.dispatch(resetState()) +console.log(store.getState()) // { num: 2, arr: [1, 2, 3], bool: true } +``` diff --git a/docs/defaults/reset.spec.js b/docs/defaults/reset.spec.js new file mode 100644 index 00000000..19dbcd5d --- /dev/null +++ b/docs/defaults/reset.spec.js @@ -0,0 +1,46 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.reset(): returns an action that, when dispatched, updates the leaf's state to the reducer's initialised state", () => { + const initialState = { + num: 2, + arr: [1, 2, 3], + bool: true + } + + const otherState = { + num: 11, + arr: ['a', 'b', 'c'], + bool: false + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer, otherState) + + test("State is the preloaded state", () => { + expect(store.getState()).toEqual(otherState) + }) + + test("Calling create.reset on a leaf", () => { + const resetNum = actions.num.create.reset + store.dispatch(resetNum()) + expect(store.getState().num).toBe(2) + }) + + test("Calling create(actionType).reset on a leaf", () => { + const resetBool = actions.bool.create.reset + store.dispatch(resetBool()) + expect(store.getState().bool).toBe(true) + }) + + + test("Calling create.reset on a branch", () => { + const resetState = actions.create.reset + store.dispatch(resetState()) + expect(store.getState()).toEqual({ + num: 2, + arr: [1, 2, 3], + bool: true + }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/set.md b/docs/defaults/set.md new file mode 100644 index 00000000..f44d15eb --- /dev/null +++ b/docs/defaults/set.md @@ -0,0 +1,48 @@ +--- +id: set +title: set +hide_title: true +sidebar_label: set +--- + +# `set(key, value)` +**`create.set`** +**`create(actionType).set`** +*Appropriate leaf type: object* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state at the property `key` with `value`. + +## Parameters +- `key` *(string)*: the path of the property to set +- `value` *(any)*: the value to set + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: {}, + bar: { props: true } +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Setting a new property +```js +const setInFoo = actions.foo.create.set +store.dispatch(setInFoo('accessed', true)) +console.log(store.getState().foo) // { accessed: true } +``` + +### Overwriting a property +```js +const setInBar = actions.bar.create.set +store.dispatch(setInBar('props', false)) +console.log(store.getState().bar) // { props: false } +``` \ No newline at end of file diff --git a/docs/defaults/set.spec.js b/docs/defaults/set.spec.js new file mode 100644 index 00000000..2118937b --- /dev/null +++ b/docs/defaults/set.spec.js @@ -0,0 +1,24 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.set(path, value): returns an action that, when dispatched, updates the leaf's state by non-mutatively setting value at state object's path", () => { + const initialState = { + foo: {}, + bar: { props: true } + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("Setting a new property", () => { + const setInFoo = actions.foo.create.set + store.dispatch(setInFoo('accessed', true)) + expect(store.getState().foo).toEqual({ accessed: true }) + }) + + test("Overwriting a property", () => { + const setInBar = actions.bar.create.set + store.dispatch(setInBar('props', false)) + expect(store.getState().bar).toEqual({ props: false }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/toggle.md b/docs/defaults/toggle.md new file mode 100644 index 00000000..18199bc2 --- /dev/null +++ b/docs/defaults/toggle.md @@ -0,0 +1,44 @@ +--- +id: toggle +title: toggle +hide_title: true +sidebar_label: toggle +--- + +# `toggle()` +**`create.toggle`** +**`create(actionType).toggle`** +*Appropriate leaf state: boolean* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `!leafState`. + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + foo: true, + bar: false +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Calling `create.toggle` +```js +const toggleFoo = actions.foo.create.toggle +store.dispatch(toggleFoo()) +console.log(store.getState().foo) // false +``` + +### Calling `create(actionType).toggle` +```js +const toggleBar = actions.bar.create('TOGGLE_BAR').toggle +store.dispatch(toggleBar()) +console.log(store.getState().bar) // true +``` \ No newline at end of file diff --git a/docs/defaults/toggle.spec.js b/docs/defaults/toggle.spec.js new file mode 100644 index 00000000..768c8f05 --- /dev/null +++ b/docs/defaults/toggle.spec.js @@ -0,0 +1,27 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.toggle(): returns an action that, when dispatched, updates the leaf's state to false", () => { + + describe("GIVEN initialState is an object", () => { + const initialState = { + foo: true, + bar: false + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test('Calling create.toggle', () => { + const turnOnFoo = actions.foo.create.toggle + store.dispatch(turnOnFoo()) + expect(store.getState().foo).toBe(false) + }) + + test('Calling create(actionType).toggle', () => { + const turnOnBar = actions.bar.create("TURN_ON_BAR").toggle + store.dispatch(turnOnBar()) + expect(store.getState().bar).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/docs/defaults/update.md b/docs/defaults/update.md new file mode 100644 index 00000000..bb9e5e9a --- /dev/null +++ b/docs/defaults/update.md @@ -0,0 +1,58 @@ +--- +id: update +title: update +hide_title: true +sidebar_label: update +--- + +# `update(value)` +**`create.update`** +**`create(actionType).update`** +*Appropriate leaf state: any* + +Returns an object that, *when dispatched to a store created with the original state tree*, updates the leaf's state to `value`. + +## Parameters +- `value` *(any)*: the new value for the leaf's state + +## Returns +`action` *(object)*: an object to dispatch to the store + +## Example +```js +import { createStore } from 'redux' +import reduxLeaves from 'reduxLeaves' + +const initialState = { + bool: false, + num: 2, + str: 'foo', + arr: [1, 2, 3] +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Calling `create.update` on a leaf: + +```js +const updateStr = actions.str.create.update +store.dispatch(updateStr("I can put anything here")) +console.log(store.getState().str) // 'I can put anything here' +``` + +### Calling `create(actionType).update` on a leaf: + +```js +const updateNum = actions.num.create('UPDATE_NUM').update +store.dispatch(updateNum(9001)) +console.log(store.getState().num) // 9001 +``` + +### Calling `create.update` on a branch: +```js +const updateState = actions.create.update +store.dispatch(updateState({ any: { properties: true }})) +console.log(store.getState()) // { any: { properties: true } } +``` \ No newline at end of file diff --git a/docs/defaults/update.spec.js b/docs/defaults/update.spec.js new file mode 100644 index 00000000..a8d3857c --- /dev/null +++ b/docs/defaults/update.spec.js @@ -0,0 +1,33 @@ +import { createStore } from "redux"; +import reduxLeaves from '../../src'; + +describe("leaf.create.update(value): returns an action that, when dispatched, updates the leaf's state to value", () => { + + const initialState = { + bool: false, + num: 2, + str: 'foo', + arr: [1, 2, 3] + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test('Calling create.update on a leaf', () => { + const updateStr = actions.str.create.update + store.dispatch(updateStr("I can put anything here")) + expect(store.getState().str).toBe('I can put anything here') + }) + + test('Calling create(actionType).update on a leaf', () => { + const updateNum = actions.num.create('UPDATE_NUM').update + store.dispatch(updateNum(9001)) + expect(store.getState().num).toBe(9001) + }) + + test('Calling create.update on a branch', () => { + const updateState = actions.create.update + store.dispatch(updateState({ any: { properties: true } })) + expect(store.getState()).toEqual({ any: { properties: true } }) + }) +}) \ No newline at end of file diff --git a/docs/examples/advancedExample.md b/docs/examples/advancedExample.md new file mode 100644 index 00000000..0ff78899 --- /dev/null +++ b/docs/examples/advancedExample.md @@ -0,0 +1,119 @@ +--- +id: advanced-example +title: Advanced example +hide_title: true +sidebar_label: Advanced example +--- + +# Advanced example: custom types and controlling payloads + +## Custom action types + +### Default action types +When you create an action through Redux-Leaves - whether using a default creator or some custom reducer logic you've supplied - it gives the action an informative `type` property: + +```js +import { createStore } from 'redux' +import reduxLeaves from 'redux-leaves' + +const initialState = { + list: ['a', 'b'], + nested: { + counter: 0, + state: { + deep: 'somewhat' + } + } +} + +const reducersDict = { + duplicate: leafState => leafState.concat(leafState) +} + +const [reducer, actions] = reduxLeaves(initialState, reducersDict) + +const actionToPushToList = actions.list.create.push('c') +console.log(actionToPushToList.type) // 'list/PUSH' + +const actionToDuplicateList = actions.list.create.duplicate() +console.log(actionToDuplicateList.type) // 'list/DUPLICATE' + +const actionToUpdateDeepState = actions.nested.state.deep.create.update('could go deeper') +console.log(actionToUpdateDeepState.payload) +// 'nested/state/deep/UPDATE' +``` + +### Overriding the default action type +You may find benefits, e.g. with Redux DevTools, to overriding the default action type. + +You can do this by providing a string argument to `create`: + +```js +const appendLetter = actions.list.create('APPEND_LETTER').push +console.log(appendLetter('c').type) // 'APPEND_LETTER' + +const duplicateList = actions.list.create('DUPLICATE_LIST').duplicate +console.log(duplicateList().type) // 'DUPLICATE LIST' +``` + +Overriding the default action type won't change how the Redux-Leaves `reducer` responds to the action: +```js +const store = createStore(reducer) +console.log(store.getState().list) // ['a', 'b'] + +store.dispatch(appendLetter('c')) +console.log(store.getState().list) // ['a', 'b', 'c'] + +store.dispatch(duplicateList()) +console.log(store.getState().list) +// ['a', 'b', 'c', 'a', 'b', 'c'] +``` + +### Usage pattern +An expected pattern that this facilitates is the defining of action creators in one file, e.g. `actions.js`: +```js +// import the actions object created by Redux-Leaves +import { actions } from './some/location' + +export const incrementCounter = actions.counter.create('INCREMENT_COUNTER').increment +export const updateDeepState = actions.nested.state.deep.create('UPDATE_DEEP_STATE').update +``` +and then import these action creators into whichever file needs access to them. + +## Controlling payloads +Suppose I want to create a custom creator, `addMultiple`, such that I can pass multiple numbers as arguments and have them all added to a given leaf's state. + +The default behaviour of a custom action creator is that only the first argument is passed as an action's payload, but we can configure that: + +```js +import { createStore } from 'redux' +import reduxLeaves from 'redux-leaves' + +const initialState = { + counter: 0 +} + +const reducersDict = { + // object configuration longhand + addMultiple: { + // Capture all arguments and pass them to the reducer: + argsToPayload: (...args) => args, + reducer: (leafState, { payload }) => payload.reduce((acc, val) => acc + val, leafState) + }, + + // function shorthand + // uses default payload behaviour + addFirstThing: (leafState, { payload }) => leafState + payload +} + +const [reducer, actions] = reduxLeaves(initialState, reducersDict) +const store = createStore(reducer) + +console.log(store.getState().counter) // 0 + +store.dispatch(actions.counter.create.addMultiple(4, 2, 10)) +console.log(store.getState().counter) // 16 + +store.dispatch(actions.counter.create.addFirstThing(1, 100)) +console.log(store.getState().counter) // 17 +``` \ No newline at end of file diff --git a/docs/examples/advancedExample.spec.js b/docs/examples/advancedExample.spec.js new file mode 100644 index 00000000..e845ad5d --- /dev/null +++ b/docs/examples/advancedExample.spec.js @@ -0,0 +1,84 @@ +import { createStore } from 'redux'; +import reduxLeaves from '../../src'; + + +describe('Advanced example', () => { + describe('Custom types', () => { + const initialState = { + list: ['a', 'b'], + nested: { + counter: 0, + state: { + deep: 'somewhat' + } + } + } + + const reducersDict = { + duplicate: leafState => leafState.concat(leafState) + } + + const [reducer, actions] = reduxLeaves(initialState, reducersDict) + + it('Creates informative action types by default', () => { + const actionToPushToList = actions.list.create.push('c') + expect(actionToPushToList.type).toBe('list/PUSH') + + const actionToDuplicateList = actions.list.create.duplicate() + expect(actionToDuplicateList.type).toBe('list/DUPLICATE') + + const actionToUpdateDeepState = actions.nested.state.deep.create.update('could go deeper') + expect(actionToUpdateDeepState.type).toBe('nested/state/deep/UPDATE') + }) + + test('You can override the default action type', () => { + const appendLetter = actions.list.create('APPEND_LETTER').push + expect(appendLetter('c').type).toBe('APPEND_LETTER') + + const duplicateList = actions.list.create('DUPLICATE_LIST').duplicate + expect(duplicateList().type).toBe('DUPLICATE_LIST') + }) + + test("Overriding action type doesn't change how the reducer responds", () => { + const store = createStore(reducer) + expect(store.getState().list).toEqual(['a', 'b']) + + store.dispatch(actions.list.create('APPEND_LETTER').push('c')) + expect(store.getState().list).toEqual(['a', 'b', 'c']) + + store.dispatch(actions.list.create('DUPLICATE_LIST').duplicate()) + expect(store.getState().list).toEqual(['a', 'b', 'c', 'a', 'b', 'c']) + }) + }) + + describe('Controlling payloads', () => { + const initialState = { + counter: 0 + } + + const reducersDict = { + addMultiple: { + argsToPayload: (...args) => args, + reducer: (leafState, { payload }) => payload.reduce((acc, val) => acc + val, leafState) + }, + addFirstThing: (leafState, { payload }) => leafState + payload + } + + const [reducer, actions] = reduxLeaves(initialState, reducersDict) + const store = createStore(reducer) + + test('We can configure to use custom argsToPayload', () => { + expect(store.getState().counter).toBe(0) + + store.dispatch(actions.counter.create.addMultiple(4, 2, 10)) + expect(store.getState().counter).toBe(16) + }) + + test("If we don't configure, it uses only the first argument as payload", () => { + expect(store.getState().counter).toBe(16) + + store.dispatch(actions.counter.create.addFirstThing(1, 100)) + expect(store.getState().counter).toBe(17) + }) + }) +}) \ No newline at end of file diff --git a/docs/examples/basicExample.md b/docs/examples/basicExample.md new file mode 100644 index 00000000..f2f713b3 --- /dev/null +++ b/docs/examples/basicExample.md @@ -0,0 +1,122 @@ +--- +id: basic-example +title: Basic example +hide_title: true +sidebar_label: Basic example +--- + +# Basic example: 30 second demo + +**Situation**: I want to be able to increment two different counters in Redux state, `counterOne` and `counterTwo`. +**Complication**: I want to do this as quickly, painlessly and intuitively as possible. +**Question**: Do I really have to define reducers, action types and creators to do this? + +Answer: no! Just provide Redux-Leaves with your state shape, i.e. the two counters, and it'll do the rest for you! + +## Demonstration + +### Set up the store's state +```js +// Imports for Redux and Redux-Leaves +import { createStore } from 'redux' +import reduxLeaves from 'redux-leaves' + +// Your job: provide some initial state +const initialState = { + counterOne: 0, + counterTwo: 0 +} + +// Redux-Leaves's job: to write your reducer and actions for you +const [reducer, actions] = reduxLeaves(initialState) + +// Create your Redux store using the given reducer +const store = createStore(reducer) +``` + +### Update the store's state +```js +console.log(store.getState()) // { counterOne: 0, counterTwo: 0 } + +// Let's create an action to increment counterOne by 3 +const actionToIncrementCounterOneByThree = actions.counterOne.create.increment(3) + +// Dispatch our created action to the store +store.dispatch(actionToIncrementCounterOneByThree) + +// The store's state will be updated! +console.log(store.getState()) // { counterOne: 3, counterTwo: 0 } + +// Now let's increment counterTwo by 10 +store.dispatch(actions.counterTwo.create.increment(10)) +console.log(store.getState()) // { counterOne: 3, counterTwo: 10 } +``` + +## Default action creators +`increment` is one of many default action creators that Redux-Leaves writes for you. + +If you want to add some custom action creators, look at the [intermediate example](intermediateExample.md). + +Here are some other common defaults that you might like to use: + +```js +import { createStore } from 'redux' +import reduxLeaves from 'redux-leaves' + +const initialState = { + arr: [3, 'things', 'here'], + bool: false, + obj: { + nested: true + } + title: 'Redux-Leaves', +} + +const [reducer, actions] = reduxLeaves(initialState) +const store = createStore(reducer) +``` + +### Arrays +```js +// push: push an element to an array +store.dispatch(actions.arr.create.push('new element')) +console.log(store.getState().arr) // [3, 'things', 'here', 'new element'] + +// drop: drop n elements from an array +store.dispatch(actions.arr.create.drop(2)) +console.log(store.getState().arr) // [3, 'things'] +``` + +### Booleans +```js +// toggle: toggles a boolean +store.dispatch(actions.bool.create.toggle()) +console.log(store.getState().bool) // true + +// off: make a boolean false (or 'on' for true) +store.dispatch(actions.bool.create.off()) +console.log(store.getState().bool) // false +``` + +### Plain objects +```js +// assign: spreads properties +store.dispatch(actions.obj.create.assign({ deep: false })) +console.log(store.getState().obj) // { nested: true, deep: false } + +// path: sets a value at a given path in the object +store.dispatch(actions.obj.create.path(['arbitrary', 'property'], 3)) +console.log(store.getState().obj.arbitrary) // { property: 3 } +``` + +### Type agnostic +```js +// apply: updates state by applying a callback +store.dispatch(actions.title.create.apply(str => str.toUpperCase()) +console.log(store.getState().title) // 'REDUX-LEAVES' + +// update: changes state to the value provided +store.dispatch(actions.title.create.update('Redux-Leaves is GREAT')) +console.log(store.getState().title) // 'Redux-Leaves is GREAT' +``` + diff --git a/docs/examples/basicExample.spec.js b/docs/examples/basicExample.spec.js new file mode 100644 index 00000000..9a0a1951 --- /dev/null +++ b/docs/examples/basicExample.spec.js @@ -0,0 +1,28 @@ +import { createStore } from 'redux'; +import reduxLeaves from '../../src'; + + +describe('Basic example', () => { + const initialState = { + counterOne: 0, + counterTwo: 0 + } + + const [reducer, actions] = reduxLeaves(initialState) + const store = createStore(reducer) + + test("Store initialises with the provided initialState", () => { + expect(store.getState()).toEqual({ counterOne: 0, counterTwo: 0 }) + }) + + test("We can increment counterOne by 3", () => { + const actionToIncrementCounterOneByThree = actions.counterOne.create.increment(3) + store.dispatch(actionToIncrementCounterOneByThree) + expect(store.getState()).toEqual({ counterOne: 3, counterTwo: 0 }) + }) + + test("We can increment counterTwo by 10", () => { + store.dispatch(actions.counterTwo.create.increment(10)) + expect(store.getState()).toEqual({ counterOne: 3, counterTwo: 10 }) + }) +}) \ No newline at end of file diff --git a/docs/examples/intermediateExample.md b/docs/examples/intermediateExample.md new file mode 100644 index 00000000..dc0c7290 --- /dev/null +++ b/docs/examples/intermediateExample.md @@ -0,0 +1,67 @@ +--- +id: intermediate-example +title: Intermediate example +hide_title: true +sidebar_label: Intermediate example +--- + +# Intermediate example: custom logic + +**Situation**: I want to define a general type of reducer logic that can be reused on any arbitrary slice of state. +**Complication**: I want to do this as quickly, painlessly and intuitively as possible. +**Question**: Do I really have to create sub-reducers with the same underlying logic? + +Answer: no! Just provide Redux-Leaves once with your custom reducer logic, and you can automatically use it at any leaf of your state tree. + +## Demonstration + +### Set up with your custom reducer logic +```js +import { createStore } from 'redux' +import reduxLeaves from 'redux-leaves' + +const initialState = { + counter: 2, + list: ['first', 'second'], + nested: { arbitrarily: { deep: 0 } } +} + +// Key your reducer logic by a descriptive verb +const reducersDict = { + double: leafState => leafState * 2, + appendToEach: (leafState, action) => leafState.map(str => str.concat(action.payload)), + countTreeKeys: (leafState, action, treeState) => Object.keys(treeState).length +} + +// Provide the dictionary of your reducer logic to reduxLeaves +const [reducer, actions] = reduxLeaves(initialState, reducersDict) +const store = createStore(reducer) +``` + +### Dispatch actions at any leaf with the corresponding keys +```js +store.dispatch(actions.counter.create.double()) +console.log(store.getState().counter) // 4 + +store.dispatch(actions.list.create.appendToEach(' item')) // ' item' will be the action payload +console.log(store.getState().list) // ['first item', 'second item'] + +store.dispatch(actions.nested.arbitrarily.deep.create.countTreeKeys()) +console.log(store.getState().nested.arbitrarily.deep) // 3 + +// And to demonstrate reusing logic at an arbitrary leaf: +store.dispatch(actions.nested.arbitrarily.deep.create.double()) +console.log(store.getState().nested.arbitrarily.deep) // 6 +``` + +## Default handling of arguments +When you supply `reduxLeaves` with custom reducer logic, it provides the corresponding action creators, e.g. `actions.list.create.appendToEach` used above. + +The *default behaviour* of these action creators is that, if they receive any arguments, *only the first argument* is passed to the created action as a payload: + +```js +const actionToAppend = actions.list.create.appendToEach('foo', 'bar') +console.log(actionToAppend.payload) // 'foo' +``` + +If you would like to customise this behaviour, look at the [advanced example](advancedExample.md). \ No newline at end of file diff --git a/docs/examples/intermediateExample.spec.js b/docs/examples/intermediateExample.spec.js new file mode 100644 index 00000000..7226b518 --- /dev/null +++ b/docs/examples/intermediateExample.spec.js @@ -0,0 +1,49 @@ +import { createStore } from 'redux'; +import reduxLeaves from '../../src'; + + +describe('Intermediate example', () => { + const initialState = { + counter: 2, + list: ['first', 'second'], + nested: { arbitrarily: { deep: 0 } } + } + + const reducersDict = { + double: leafState => leafState * 2, + appendToEach: (leafState, action) => leafState.map(str => str.concat(action.payload)), + countTreeKeys: (leafState, action, treeState) => Object.keys(treeState).length + } + + const [reducer, actions] = reduxLeaves(initialState, reducersDict) + const store = createStore(reducer) + + test("We can double the counter's state", () => { + expect(store.getState().counter).toBe(2) + store.dispatch(actions.counter.create.double()) + expect(store.getState().counter).toBe(4) + }) + + test("We can append to the list", () => { + expect(store.getState().list).toEqual(['first', 'second']) + store.dispatch(actions.list.create.appendToEach(' item')) + expect(store.getState().list).toEqual(['first item', 'second item']) + }) + + test("We can count the number of keys in the state tree", () => { + expect(store.getState().nested.arbitrarily.deep).toBe(0) + store.dispatch(actions.nested.arbitrarily.deep.create.countTreeKeys()) + expect(store.getState().nested.arbitrarily.deep).toBe(3) + }) + + test("We can double arbitrarily deep state", () => { + expect(store.getState().nested.arbitrarily.deep).toBe(3) + store.dispatch(actions.nested.arbitrarily.deep.create.double()) + expect(store.getState().nested.arbitrarily.deep).toBe(6) + }) + + test("By default, when providing arguments, the first becomes the payload", () => { + const actionToAppend = actions.list.create.appendToEach('foo', 'bar') + expect(actionToAppend.payload).toBe('foo') + }) +}) \ No newline at end of file diff --git a/docs/intro/README.md b/docs/intro/README.md index 05859474..84c9ecee 100644 --- a/docs/intro/README.md +++ b/docs/intro/README.md @@ -10,7 +10,7 @@ The guiding philosophy of Redux-Leaves is *"write once, reduce anywhere"*. This page explains more about the motivation of Redux-Leaves and how its design philosophy is put into practice. -> **Just want to see some code? Check out the [30 second demo](demo.md) or [Code Sandbox](https://codesandbox.io/s/reduxleaves-iwc4f).** +> **Just want to see some code? Check out the basic [30 second demo](examples/basicExample.md).** ## Motivation diff --git a/docs/intro/demo.md b/docs/intro/demo.md deleted file mode 100644 index a83a5f4d..00000000 --- a/docs/intro/demo.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -id: demo -title: 30 Second Demo -hide_title: true -sidebar_label: 30 second demo ---- - -# Redux-Leaves - -**[Write once](#write-once). [Reduce anywhere](#reduce-anywhere).** - -## Write once. - -```js -import { createStore } from 'redux' -import reduxLeaves from 'redux-leaves' - -// 1. Define initialState -const initialState = { - counter: 1, - list: ['first'], - nested: { arbitrarily: { deep: 0 } } -} - -// 2. (Optional) Define custom reducers dictionary -const reducersDict = { - double: leafState => leafState * 2, - appendToEach: (leafState, action) => leafState.map(str => str.concat(action.payload)), - countKeys: (leafState, action, wholeState) => Object.keys(wholeState).length -} - -// 3. Grab reducer and actions from reduxLeaves, then create the Redux store -const [reducer, actions] = reduxLeaves(initialState, reducersDict) -const store = createStore(reducer) -``` - -## Reduce anywhere. - -```js -// *** KEY *** -// Default: an action creator that ships with Redux-Leaves by default -// Custom: an action creator generated by the custom reducersDict - -// Default: increment state.counter -store.dispatch(actions.counter.create.increment()) -console.log(store.getState().counter) // 2 - -// Custom: double state.counter -store.dispatch(actions.counter.create.double()) -console.log(store.getState().counter) // 4 - -// Default: push 'second' to state.list -store.dispatch(actions.list.create.push('second')) -console.log(store.getState().list) // ['first', 'second'] - -// Custom: append ' item' to each element in state.list -store.dispatch(actions.list.create.appendToEach(' item')) -console.log(store.getState().list) // ['first item', 'second item'] - -// Default: assign true to key 'newKey' at the top-level of state -store.dispatch(actions.create.assign({ newKey: true })) -console.log(store.getState().newKey) // true - -// Custom: update state.nested.arbitrarily.deep with state's number of top-level keys -store.dispatch(actions.nested.arbitrarily.deep.create.countKeys()) -console.log(store.getState().nested.arbitrarily.deep) // 4 - -console.log(store.getState()) -/* - { - counter: 4, - list: ['first item', ' second item'], - nested: { arbitrarily: { deep: 4 } } - } -*/ -``` diff --git a/docs/intro/demo.spec.js b/docs/intro/demo.spec.js deleted file mode 100644 index afe5ecce..00000000 --- a/docs/intro/demo.spec.js +++ /dev/null @@ -1,147 +0,0 @@ -import reduxLeaves from "../../src"; -import { createStore } from "redux"; - -describe("Redux-Leaves. Write once. Reduce anywhere.", () => { - describe("GIVEN initialState, reducers, reducersDict, actions and initialised redux store", () => { - const initialState = { - counter: 1, - list: ['first'], - nested: { arbitrarily: { deep: 0 } } - } - - const reducersDict = { - double: leafState => leafState * 2, - splice: { - argsToPayload: (first, second) => [first, second], - mutate: true, - reducer: (leafState, { payload }) => { leafState.splice(...payload) } - }, - appendToEach: (leafState, action) => leafState.map(str => str.concat(action.payload)), - countKeys: (leafState, action, wholeState) => Object.keys(wholeState).length - } - - const [reducer, actions] = reduxLeaves(initialState, reducersDict) - - let store - - beforeEach(() => store = createStore(reducer)) - - test("THEN actions has defined action creators for double, appendToEach and countKeys", () => { - [actions.counter, actions.list, actions.nested, actions.nested.arbitrarily, actions.nested.arbitrarily.deep].forEach( - leaf => { - expect(typeof leaf.create.double).toBe("function") - expect(typeof leaf.create.appendToEach).toBe("function") - expect(typeof leaf.create.countKeys).toBe("function") - } - ) - }) - - describe("WHEN we dispatch actions.counter.create.increment() to the store", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.increment()) - }) - - test("THEN the store's state.counter increments non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 2, - list: ['first'], - nested: { arbitrarily: { deep: 0 } } - }) - expect(initialState.counter).toBe(1) - }) - - describe("AND we dispatch actions.counter.create.double() to the store", () => { - beforeEach(() => { - store.dispatch(actions.counter.create.double()) - }) - - test("THEN the store's state.counter doubles non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 4, - list: ['first'], - nested: { arbitrarily: { deep: 0 } } - }) - expect(initialState.counter).toBe(1) - }) - - describe("AND we dispatch actions.list.create.push('second') to the store", () => { - beforeEach(() => { - store.dispatch(actions.list.create.push('second')) - }) - - test("THEN the store's state.list updates non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 4, - list: ['first', 'second'], - nested: { arbitrarily: { deep: 0 } } - }) - expect(initialState.list).toEqual(['first']) - }) - - describe("AND we dispatch actions.list.create.appendToEach(' item') to the store", () => { - beforeEach(() => { - store.dispatch(actions.list.create.appendToEach(' item')) - }) - - test("THEN the store's state.list updates non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 4, - list: ['first item', 'second item'], - nested: { arbitrarily: { deep: 0 } } - }) - expect(initialState.list).toEqual(['first']) - }) - - describe("AND we dispatch actions.create.assign({ newKey: true })", () => { - beforeEach(() => { - store.dispatch(actions.create.assign({ newKey: true })) - }) - - test("THEN the store's state.newKey updates non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 4, - list: ['first item', 'second item'], - nested: { arbitrarily: { deep: 0 } }, - newKey: true - }) - expect(initialState.newKey).not.toBeDefined() - }) - - describe("AND we dispatch actions.nested.arbitrarily.deep.create.countKeys()", () => { - beforeEach(() => { - store.dispatch(actions.nested.arbitrarily.deep.create.countKeys()) - }) - - test("THEN the store's state.nested.arbitrarily.deep updates non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 4, - list: ['first item', 'second item'], - nested: { arbitrarily: { deep: 4 } }, - newKey: true - }) - expect(initialState.nested.arbitrarily.deep).toBe(0) - }) - - describe("AND we dispatch actions.list.create.splice(0, 1)", () => { - beforeEach(() => { - store.dispatch(actions.list.create.splice(0, 1)) - }) - - test("THEN the store's state.list updates non-mutatively", () => { - expect(store.getState()).toEqual({ - counter: 4, - list: ['second item'], - nested: { arbitrarily: { deep: 4 } }, - newKey: true - }) - expect(initialState.list).toEqual(['first']) - }) - }) - }) - }) - }) - }) - }) - }) - }) -}) \ No newline at end of file diff --git a/docs/leaf/README.md b/docs/leaf/README.md deleted file mode 100644 index e5044e89..00000000 --- a/docs/leaf/README.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -id: about -title: Leaves in Redux-Leaves -hide_title: true -sidebar_label: About 'leaves' ---- - -# 'Leaves' in Redux-Leaves - -*Leaves* are a simple but important concept in Redux-Leaves. - -## In short -- [**Every node** of the initial state shape is a leaf](#every-node-of-the-initial-state-shape-is-a-leaf); and -- [**Nothing else** is a leaf](#nothing-else-is-a-leaf). - -In particular: *not every node of the Redux state is a leaf*, since the Redux state can have nodes that weren't present in the [initial state shape passed to `reduxLeaves`](../README.md#initialstate). - -Additionally: -- [The `actions` object also contains every leaf](#the-actions-object-also-contains-every-leaf); and -- [Every leaf of `actions` has a `create` property](#every-leaf-of-actions-has-a-create-property). - -This is explored further through a worked example of a simple todo app. - -## Leaves and state - -### Every node of the initial state shape is a leaf - -First, let's set up, assuming that we have defined [`reducersDict`](../README.md#reducersdict) elsewhere. - -```js -import { createStore } from 'redux' -import reduxLeaves from 'redux-leaves' -import reducersDict from './path/to/reducersDict' - -const initialState = { - todos: { - byId: {}, - allIds: [] - }, - visibilityFilter: "SHOW_ALL" -} - -const [reducer, actions] = reduxLeaves(initialState, reducersDict) -``` -[From before:](#in-short) -> - **Every node** of the initial state shape is a leaf; -> - **Nothing else** is a leaf. - -Thus, each of the following is a leaf: -- `todos`; -- `todos.byId`; -- `todos.allIds`; and -- `visibilityFilter`. - -Additionally, we can consider the whole state tree to be a leaf (as the ancestral node). - -Here are some non-leaves: -- `{ byId: {}, allIds: [] }`: this is the initial state of a leaf, but not a leaf itself; -- `SHOW_ALL`: this is the initial state of a leaf, but not a leaf itself; -- `todos.current`: there is no `todos.current` provided in the initial state shape; and -- `todos.byId.007`: there is no `todos.byId.007` provided in the initial state shape. - -### Nothing else is a leaf -(In particular: **not every node of the Redux state is a leaf**.) - -Having established what are and what are not leaves, let's now create our Redux store, and [hydrate it with some preloaded state](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer): - -```js -const preloadedState = { - todos: { - byId: { - f23f: { - title: "Read a book", - completed: true - } - }, - allIds: ['f23f'], - current: 'f23f' - }, - visibilityFilter: "SHOW_INCOMPLETE" -} - -const store = createStore(initialState, preloadedState) -``` -The following are all still leaves: -- `todos`; -- `todos.byId`; -- `todos.allIds`; and -- `visibilityFilter`. - -However, neither `todos.byId.f23f` nor `todos.current` have become leaves. - -(Even though they are nodes in the Redux state tree, since they were not nodes on the initial state shape, they are not leaves.) - -## Leaves and action creators - -### The `actions` object also contains every leaf - -Every leaf is defined as an object on `actions`. -```js -console.log(typeof actions.todos) // object -console.log(typeof actions.todos.allIds) // object -console.log(typeof actions.visibilityFilter) // object - -// Non-leaves are not defined: -console.log(typeof actions.todos.current) // undefined - todos.current is not a leaf -``` - -### Every leaf of `actions` has a [`create`](../create/defaults.md) property - -Each of these action leaves has a [`create`](../create/defaults.md) property that is an object: - -```js -console.log(typeof actions.todos.create) // object -console.log(typeof actions.todos.allIds.create) // object -console.log(typeof actions.visibilityFilter.create) // object - -// Won't work for non-leaves under actions -console.log(typeof actions.todos.byId.f23f.create) // Uncaught TypeError - todos.byId.f23f is not a leaf -console.log(typeof actions.todos.current.create) // Uncaught TypeError - todos.current is not a leaf -``` - -Every actions leaf can therefore create actions appropriate to that leaf through the [`create` API](../create/defaults.md). \ No newline at end of file diff --git a/docs/leaf/standardActions.md b/docs/leaf/standardActions.md deleted file mode 100644 index febbfe6c..00000000 --- a/docs/leaf/standardActions.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: standard-actions -title: Leaf-Standard-Actions -hide_title: true -sidebar_label: Leaf-Standard-Actions ---- - -# Leaf-Standard-Actions (LSA) - -Redux-Leaves builds upon the '[Flux Standard Action](https://github.com/redux-utilities/flux-standard-action)' (FSA) framework. - -## Background -The common Redux standard for reducers is that they [use an action's `type` property](https://redux.js.org/faq/reducers#do-i-have-to-use-the-switch-statement-to-handle-actions) to decide which logic to trigger. - -It is advised that [`type` should be a string, or at least serializable](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants). - -`type` is also used prominently in [Redux DevTools](http://extension.remotedev.io/) on that assumption. - -Although Redux doesn't require that actions be FSA, it is [given as a recommendation](https://redux.js.org/glossary#action). - -### Recap of FSA -[Definition of a FSA](https://github.com/redux-utilities/flux-standard-action/blob/master/README.md#actions): -> An action MUST -> -> * be a plain JavaScript object. -> * have a `type` property. -> -> An action MAY -> -> * have an `error` property. -> * have a `payload` property. -> * have a `meta` property. -> -> An action MUST NOT include properties other than `type`, `payload`, `error`, and `meta`. - - -## Motivation - -There *could conceivably* be a conflict between: -- `type` being maximally useful for Redux DevTools debugging ('the *descriptive* imperative'); and -- `type` being maximally useful as a director of reducer logic ('the *procedural* imperative'). - -However, there is no intrinsic reason why reducers *have* to look at `type` to decide what to do. - -As such, Redux-Leaves takes the design decision to separate out the *descriptive* and the *procedural* imperative, by: - -- reserving the `type` property for the descriptive imperative; and -- introducing a `leaf` property for the procedural imperative. - -## The descriptive imperative: `type` - -The roadmap for Redux-Leaves includes adding a [configuration key](../leafReducers.md#configuration-keys) so that developers can customise the `type` used by a given [action creator](../create/README.md). - -The default (and, for now, only) behaviour is such that: - -```js -const action = actions.foo.bar.create.myCustomActionCreator() -console.log(action.type) // foo/bar/MY_CUSTOM_ACTION_CREATOR -``` - -## The procedural imperative: `leaf` - -In order to free `type` up to focus entirely on the *descriptive* imperative, Redux-Leaves introduces the **`leaf`** property to take care of the *procedural* imperative. - -```js -const action = actions.foo.bar.create.myCustomActionCreator() -console.log(action.leaf) -/* -{ - path: ['foo', 'bar'], - creatorKey: 'myCustomActionCreator', - CREATOR_KEY: 'MY_CUSTOM_ACTION_CREATOR' - custom: true -} -/* -``` -### Properties -- `path` -- `creatorKey` -- `CREATOR_KEY` -- `custom` - diff --git a/package.json b/package.json index 41cf27a7..f364c103 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-leaves", - "version": "0.6.0", + "version": "0.6.1", "description": "Write once. Reduce anywhere", "main": "dist/index.js", "license": "MIT", diff --git a/src/actions/create/defaults/makeCreateDefaults.ts b/src/actions/create/defaults/makeCreateDefaults.ts index e14ec9ed..e230e7d6 100644 --- a/src/actions/create/defaults/makeCreateDefaults.ts +++ b/src/actions/create/defaults/makeCreateDefaults.ts @@ -21,7 +21,7 @@ const makeCreateDefaults = (path: string[]) => (actionType?: string | LeafAction on: () => producerOfLeafStandardActions(atomicActions.ON)(), path: (path: string[], value: any) => producerOfLeafStandardActions(atomicActions.SET)({ path, value }), push: (element: any, index: number = -1, replace: boolean = false) => producerOfLeafStandardActions(atomicActions.PUSH)({ element, index, replace }), - replace: (pattern: string | RegExp, replacement: string) => producerOfLeafStandardActions(atomicActions.REPLACE)({ pattern, replacement }), + // replace: (pattern: string | RegExp, replacement: string) => producerOfLeafStandardActions(atomicActions.REPLACE)({ pattern, replacement }), reset: () => producerOfLeafStandardActions(atomicActions.RESET)(), set: (key: string, value: any) => producerOfLeafStandardActions(atomicActions.SET)({ path: [key], value }), toggle: () => producerOfLeafStandardActions(atomicActions.TOGGLE)(), diff --git a/src/leafReducer/custom/leafReducerCustom.ts b/src/leafReducer/custom/leafReducerCustom.ts index 17dbb652..1ef6b5e1 100644 --- a/src/leafReducer/custom/leafReducerCustom.ts +++ b/src/leafReducer/custom/leafReducerCustom.ts @@ -12,13 +12,7 @@ const leafReducerCustom: LeafReducerTyped = (leafState, action, wholeState, redu } const applyReducer = (config: LeafReducerConfig, leafState: any, action: LeafStandardAction, wholeState: any) => { - if (config.mutate) { - return produce(leafState, (draftLeafState: any) => { - config.reducer(draftLeafState, action, wholeState) - }) - } else { - return config.reducer(leafState, action, wholeState) - } + return config.reducer(leafState, action, wholeState) } export default leafReducerCustom \ No newline at end of file diff --git a/src/leafReducer/leafReducer.ts b/src/leafReducer/leafReducer.ts index 84ab9409..79c87c00 100644 --- a/src/leafReducer/leafReducer.ts +++ b/src/leafReducer/leafReducer.ts @@ -39,7 +39,7 @@ export const leafReducer = ( case atomicActions.OFF: return false case atomicActions.ON: return true case atomicActions.PUSH: return push(draftLeafState, payload) - case atomicActions.REPLACE: return replace(draftLeafState, payload) + // case atomicActions.REPLACE: return replace(draftLeafState, payload) case atomicActions.RESET: return reset(initialWhole, path) case atomicActions.SET: return set(draftLeafState, payload) case atomicActions.TOGGLE: return !draftLeafState @@ -90,10 +90,10 @@ const push = ( : insertAtIndex(leafState, index, element) ) -const replace = ( - leafState: string, - { pattern, replacement }: { pattern: string | RegExp, replacement: string } -) => leafState.replace(pattern, replacement) +// const replace = ( +// leafState: string, +// { pattern, replacement }: { pattern: string | RegExp, replacement: string } +// ) => leafState.replace(pattern, replacement) const reset = (initialWholeState: any, path: string[]) => ( path.length >= 1 ? R.path(path, initialWholeState) : initialWholeState diff --git a/website/pages/en/index.js b/website/pages/en/index.js index 0775a9a0..1a1c85b6 100644 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -65,7 +65,7 @@ class HomeSplash extends React.Component { - + @@ -169,7 +169,7 @@ class Index extends React.Component { }, { title: 'Quick setup.', - content: `With multiple action creators built in by default, it takes just 30 seconds to get up and running.`, + content: `With multiple action creators built in by default, it takes just 30 seconds to get up and running.`, image: `${baseUrl}img/fast.png`, imageAlign: 'top', }, diff --git a/website/sidebars.json b/website/sidebars.json index c0a6794c..ba3d1518 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -2,31 +2,72 @@ "docs": { "Introduction": [ "intro/overview", - "intro/demo", "intro/features" ], - "Leaves & Reducers":[ - "redux-leaves", - "leaf/about", - "leaf-reducers", - "creator-keys" + "Examples":[ + "examples/basic-example", + "examples/intermediate-example", + "examples/advanced-example" ], - "Actions": [ - "create/creators", - "leaf/standard-actions" + "API Reference": [ + "redux-leaves", + "api/leaf-reducers", + "api/creator-keys", + "api/actions", + "api/create" ], "Default creators": [ - "create/defaults", - "create/type-specific", + "defaults/by-type", + { + "type": "subcategory", + "label": "any", + "ids": [ + "create/apply", + "create/clear", + "create/reset", + "create/update" + ] + }, + { + "type": "subcategory", + "label": "array", + "ids": [ + "create/concat", + "create/drop", + "create/filter", + "create/push" + ] + }, + { + "type": "subcategory", + "label": "boolean", + "ids": [ + "create/off", + "create/on", + "create/toggle" + ] + }, + { + "type": "subcategory", + "label": "number", + "ids": [ + "create/increment" + ] + }, + { + "type": "subcategory", + "label": "object", + "ids": [ + "create/assign", + "create/path", + "create/set" + ] + }, { "type": "subcategory", - "label": "create.as", + "label": "string", "ids": [ - "create/asArray/array-creators", - "create/asBoolean/boolean-creators", - "create/asNumber/number-creators", - "create/asObject/object-creators", - "create/asString/string-creators" + "create/concat" ] } ]