# Gnar Edge: Precision edging for JS apps

[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
[![codecov](https://codecov.io/gl/gnaar/edge/branch/master/graph/badge.svg?token=MRowdXaujg)](https://codecov.io/gl/gnaar/edge)
[![pipeline status](https://gitlab.com/gnaar/edge/badges/master/pipeline.svg)](https://gitlab.com/gnaar/edge/commits/master)
[![npm version](https://badge.fury.io/js/gnar-edge.svg)](https://badge.fury.io/js/gnar-edge)
[![total downloads of gnar-edge](https://img.shields.io/npm/dt/gnar-edge.svg)](https://www.npmjs.com/package/gnar-edge)

**Part of Project Gnar:** &nbsp;[base](https://hub.docker.com/r/gnar/base) &nbsp;•&nbsp; [gear](https://pypi.org/project/gnar-gear) &nbsp;•&nbsp; [piste](https://gitlab.com/gnaar/piste) &nbsp;•&nbsp; [off-piste](https://gitlab.com/gnaar/off-piste) &nbsp;•&nbsp; [edge](https://www.npmjs.com/package/gnar-edge) &nbsp;•&nbsp; [powder](https://gitlab.com/gnaar/powder) &nbsp;•&nbsp; [genesis](https://gitlab.com/gnaar/genesis) &nbsp;•&nbsp; [patrol](https://gitlab.com/gnaar/patrol)

**Get started with Project Gnar on** &nbsp;[![Project Gnar on Medium](https://s3-us-west-2.amazonaws.com/project-gnar/medium-68x20.png)](https://medium.com/@ic3b3rg/project-gnar-d274165793b6)

**Join Project Gnar on** &nbsp;[![Project Gnar on Slack](https://s3-us-west-2.amazonaws.com/project-gnar/slack-69x20.png)](https://join.slack.com/t/project-gnar/shared_invite/enQtNDM1NzExNjY0NjkzLWQ3ZTQyYjgwMjkzNWYxNDJiNTQzODY0ODRiMmZiZjVkYzYyZWRkOWQzNjA0OTk3NWViNWM5YTZkMGJlOGIzOWE)

**Support Project Gnar on** &nbsp;[![Project Gnar on Patreon](https://s3-us-west-2.amazonaws.com/project-gnar/patreon-85x12.png)](https://patreon.com/project_gnar)

**Gnar Edge is a sharp set of JS utilities:** &nbsp;[base64](#base64) &nbsp;•&nbsp; [drain](#drain) &nbsp;•&nbsp; [handleChange](#handlechange) &nbsp;•&nbsp; [JWT](#jwt) &nbsp;•&nbsp; [notifications](#notifications) &nbsp;•&nbsp; [redux](#redux)

#### Installation

```bash
npm install gnar-edge
```

or

```bash
yarn add gnar-edge
```

#### Packages

Gnar Edge ships with three distinct types of packages:
- Single optimized (45.0 KB) ES5 main package
- Discreet ES5 tree-shakable packages (sizes listed below)
- ES6 tree-shakable modules (recommended)

Choose only one type of package to use in your application; mixing package types will needlessly increase the size of your production build.

The ES5 packages are transpiled via Babel using the following [browserslist](https://www.npmjs.com/package/browserslist) setting:

```json
"browserslist": [ ">0.25%", "ie >= 11" ]
```

This is a fairly conservative setting, [largely due to the inclusion of IE 11](https://caniuse.com/#compare=ie+11,edge+17,chrome+68) (scroll down), and may result in build that is larger than necessary for your specific needs. The overall browser market share of that setting can be found at [browserl.ist](http://browserl.ist/?q=%3E0.25%25%2C%20ie%20%3E%3D%2011), however the global coverage listed there is a bit misleading. Babel uses the [lowest common denominator](https://babeljs.io/docs/en/babel-preset-env/#determine-the-lowest-common-denominator-of-plugins-to-be-included-in-the-preset) of all the ES6 features in your code vs. the [target browsers' ES6 support](https://caniuse.com/#comparison) to determine which shims to include in the build. The global market share of browsers that support your code with the bundled shims is likely much higher. If you prefer to transpile Gnar Edge using a different set of target browsers, use [Gnar Edge's ES6](#es6-tree-shakable-modules) modules.

Gnar Edge is written in ES6+. See the [ES6 Tree-Shakable Modules](#es6-tree-shakable-modules) section for more info.

#### Main Package

Chose the main package when want ES5 code and you plan to use all the Gnar Edge utilities in your app *or* when the combined size of the utilities (listed below) you choose to use exceeds 45.0 KB.

The main package is smaller than the combined total of the tree-shakable packages (45.0 KB vs. 54.7 KB) due to the webpack module overhead, module overlap (i.e. `base64` and `jwt`) and overlap of the babel shims between modules.

The main package may grow over time. There is a high probability that new utilities will be added to Gnar Edge in the future and all new utilities will be added to the main package. Whenever a new utility is added, Gnar Edge's major semver will be incremented (e.g. `1.x.x` -> `2.0.0`).

##### Usage

The main package can be used as an [ES6 import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) or a [Node require](https://nodejs.org/api/modules.html#modules_modules):

```javascript
import { base64, drain, handleChange, jwt, notifications } from 'gnar-edge';
```

or

```javascript
const { base64, drain, handleChange, jwt, notifications } = require('gnar-edge');
```

In the module docs and code examples, we'll be using the ES6 format.

#### ES5 Tree-Shakable Packages

If you want ES5 code and you only need some of Gnar Edge's utilities, we can take advantage of Webpack's [tree shaking](https://webpack.js.org/guides/tree-shaking) to reduce the size of your production build.

The tree-shakable packages are:

Package | Size
-- | --:
gnar-edge/base64 | 2.3 KB
gnar-edge/drain | 3.5 KB
gnar-edge/handleChange | 2.8 KB
gnar-edge/jwt | 4.2 KB
gnar-edge/notifications | 35.6 KB
gnar-edge/redux | 6.3 KB

##### Usage

Each tree-shakable package can be used as an [ES6 import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) or a [Node require](https://nodejs.org/api/modules.html#modules_modules), for example:

```javascript
import base64 from 'gnar-edge/base64';
```

or

```javascript
const base64 = require('gnar-edge/base64').default;
```

In the module docs and code examples, we'll be using the ES6 format.

#### ES6 Tree-Shakable Modules

Gnar Edge is written in ES6+, i.e. ES6 mixed with a few features that are in the [TC39 process](https://tc39.github.io/process-document), namely the [bind operator](https://babeljs.io/blog/2015/05/14/function-bind) (`::`) and [decorators](http://babeljs.io/docs/en/babel-plugin-transform-decorators). Using Gnar Edge's ES6 modules will significantly reduce your production build size vs. using the ES5 packages, but it does require extra setup work. The ES6 modules that ship with Gnar Edge are not minified (minification / uglification should be part of your build process).

The ES6 modules are:

Module | Size
-- | --:
gnar-edge/es/base64 | 0.7 KB
gnar-edge/es/drain | 2.1 KB
gnar-edge/es/handleChange | 0.6 KB
gnar-edge/es/jwt | 2.1 KB
gnar-edge/es/notifications | 19.6 KB
gnar-edge/es/redux | 3.3 KB

The minified module sizes reported above are produced in the [Gnar Powder](https://gitlab.com/gnaar/powder) build. YMMV.

The total gzipped size of gnar-edge (excluding drain) is 4.6 KB. This is really the most noteworthy number since it is the number of bytes that will be transmitted over the wire if your server is properly configured.

To use the ES6 modules with Babel 7, follow these steps:

1. Install the following babel plugins:

    ```bash
    npm i -D \
      @babel/plugin-syntax-dynamic-import \
      @babel/plugin-proposal-function-bind \
      @babel/plugin-proposal-export-default-from \
      @babel/plugin-proposal-decorators \
      @babel/plugin-proposal-class-properties
    ```

1. Add these plugins to your `babel.config.js` file, e.g.

    ```javascript
    {
      presets: [
        '@babel/preset-env',
        '@babel/react'
      ],
      env: {
        production: {
          presets: [ 'react-optimize' ]
        }
      },
      plugins: [
        'react-hot-loader/babel',
        '@babel/transform-runtime',
        '@babel/plugin-syntax-dynamic-import',
        '@babel/plugin-proposal-function-bind',
        '@babel/plugin-proposal-export-default-from',
        [ '@babel/plugin-proposal-decorators', { legacy: true } ],
        [ '@babel/plugin-proposal-class-properties', { loose: true } ]
      ]
    }
    ```

    Check the [Edge](https://gitlab.com/gnaar/edge) and [Powder](https://gitlab.com/gnaar/powder) repos for complete examples of `package.json` and `babel.config.js`.

1. Update your webpack module rule for javascript to *not* exclude `gnar-edge`. You will likely have a js rule that looks like:

    ```javascript
    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
    ```

    Change the rule to:

    ```javascript
    {
      test: /\.jsx?$/,
      exclude: /node_modules\/(?!gnar-edge)/,
      use: 'babel-loader'
    }
    ```

    This tells babel to transpile the gnar-edge code along with your application code.

1. If you're linting with eslint, add `"legacyDecorators": true` to `parserOptions.ecmaFeatures` in `.eslintrc`.

1. If you're testing with Jest, add or update `transformIgnorePatterns` in `package.json` to exclude `gnar-edge`, e.g.

    ```json
    "transformIgnorePatterns": [
      "<rootDir>/node_modules/(?!gnar-edge)"
    ],
    ```

    This tells Jest to transpile the gnar-edge code.

1. Use the Gnar Edge `es` modules in your app, e.g.:

    ```javascript
    import base64 from 'gnar-edge/es/base64';
    ```

## Base64

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/base64.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/__tests__/base64.test.js)

**Usage:**

Using the [ES5 main package](#main-package):

```javascript
import { base64 } from 'gnar-edge';
```

or, using the [ES5 tree-shakable package](#es5-tree-shakable-packages):

```javascript
import base64 from 'gnar-edge/base64';
```

or, using the [ES6 tree-shakable module](#es6-tree-shakable-modules):

```javascript
import base64 from 'gnar-edge/es/base64';
```

Read the [package usage](#packages) section if you're unsure of which format to use.

The `base64` package provides three simple functions for encoding and decoding base64 content:
- `base64.decode` handles both traditional and [web-safe](https://docs.python.org/2/library/base64.html#base64.urlsafe_b64encode) base64 content, outputs a UTF-8 string
- `base64.encode` encodes a UTF-8 string to web-safe base64
- `base64.encodeNonWebSafe` encode a UTF-8 string to traditional base64

**Examples:**

```javascript
base64.encode('✓ à la mode');           // '4pyTIMOgIGxhIG1vZGU='
base64.decode('4pyTIMOgIGxhIG1vZGU=');  // '✓ à la mode'
base64.encode('  >');                   // 'ICA-'
base64.encode('  ?');                   // 'ICA_'
base64.encodeNonWebSafe('  >');         // 'ICA+'
base64.encodeNonWebSafe('  ?');         // 'ICA/'
```

Acknowledgements
 - [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding) offers two alternative solutions for transcoding Unicode to Base64.

## Drain

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/drain.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/__tests__/drain.test.js)

**Usage:**

Using the [ES5 main package](#main-package):

```javascript
import { drain } from 'gnar-edge';
```

or, using the [ES5 tree-shakable package](#es5-tree-shakable-packages):

```javascript
import drain from 'gnar-edge/drain';
```

or, using the [ES6 tree-shakable module](#es6-tree-shakable-modules):

```javascript
import drain from 'gnar-edge/es/drain';
```

Read the [package usage](#packages) section if you're unsure of which format to use.

Drain converts a generator function to a promise. It supports yields of all JS types, i.e.:
- Functions / Thunks
- Promises
- Generator Functions
- Generators
- Async Functions
- Arrays (recursively)
- Plain (i.e. Literal) Objects (recursively)
- Basic JS Types (Number, String, Boolean, Date, etc.)

**Example:**

```javascript
drain(function* () {
  let result = 1;
  result *= yield 2;
  const array = yield [3];
  result *= array[0];
  const object = yield { x: 4 };
  result *= object.x;
  result *= yield new Promise(resolve => { setTimeout(() => { resolve(5); }, 10); });
  result *= yield () => 6;
  result *= yield () => new Promise(resolve => { setTimeout(() => { resolve(7); }, 10); });
  const mixedArray = yield [
    8,
    new Promise(resolve => { setTimeout(() => { resolve(9); }, 10); }),
    () => new Promise(resolve => { setTimeout(() => { resolve(10); }, 10); })
  ];
  mixedArray.forEach(x => { result *= x; });
  const mixedObject = yield {
    a: 11,
    b: new Promise(resolve => { setTimeout(() => { resolve(12); }, 10); }),
    c: () => new Promise(resolve => { setTimeout(() => { resolve(13); }, 10); })
  };
  Object.values(mixedObject).forEach(x => { result *= x; });
  function* generatorFunction1() {
    return yield 14;
  }
  result *= yield generatorFunction1;
  function* generatorFunction2(x) {
    return yield x;
  }
  const generator = generatorFunction2(15);
  result *= yield generator;
  result *= yield async () => {
    try {
      return await new Promise(resolve => { setTimeout(() => { resolve(16); }, 10); });
    } catch (e) {
      throw e;
    }
  };
  return result;
})
  .then(result => { console.log(result); /* 20922789888000, i.e. 16! */ });
```

#### Implementation Note

Drain returns a function which returns a promise. The returned function includes three convenience methods, `then`, `catch`, and `finally` which invoke the function and chain onto the resulting promise.

The six basic methods of utilizing `drain` are:
- as a function:

  ```javascript
  const fn = drain(function* () {});
  ```

- as a promise:

  ```javascript
  const promise = drain(function* () {})();
  ```

- `then` chained:

  ```javascript
  drain(function* () { return yield 'oh, yeah!'; })
    .then(result => { console.log(result); });
  ```

  ```bash
  >> 'oh, yeah!'
  ```

- `then` chained with `error` support:

  ```javascript
  drain(function* () { throw new Error('oops'); yield 'unreachable'; }).then(
    result => { console.log(result); },
    error => { console.log(error.message);
  });
  ```

  ```bash
  >> 'oops'
  ```

- `catch` chained:

  ```javascript
  drain(function* () { throw new Error('oops, I did it again'); yield 'unreachable'; })
    .catch(error => { console.log(error.message); });
  ```

  ```bash
  >> 'oops, I did it again'
  ```

- `finally` chained:

  ```javascript
  drain(function* () { throw new Error('oops'); yield 'unreachable'; })
    .finally(() => { console.log('always called'); });
  ```

  ```bash
  >> 'always called'
  ```

#### Usage with Jest

Testing generator functions in Jest is simple with `drain`.

**Example:**

```javascript
describe('Testing a generator function', drain(function* () {
  const theAnswerToLifeTheUniverseAndEverything = yield 42;
  expect(theAnswerToLifeTheUniverseAndEverything).toBe(42);
}));
```

Acknowledgements

[co](https://github.com/tj/co) by [@tj](https://github.com/tj) provides similar functionality to `drain`.

I initially used `co` in Project Gnar. I wrote `drain` as an enhancement to `co` to add these features:
- handle basic JS types (numbers, strings, booleans, dates, etc)
- handle functions and thunks without a callback (i.e. co's `done`)
- fix an issue with `co.wrap` - I had to override `co.wrap` to get it to pass along the generator function in my Jest tests
- provide a single dual-purpose interface, i.e. `drain` replaces both `co` and `co.wrap`
- ES6 implementation - `co` is written in ES5 whereas `drain` is written in ES6 and transpiled via Babel
- Simplified implementation: 43 SLOC vs. 101

## HandleChange

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/handleChange.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/__tests__/handleChange.test.js)

**Usage:**

Using the [ES5 main package](#main-package):

```javascript
import { handleChange } from 'gnar-edge';
```

or, using the [ES5 tree-shakable package](#es5-tree-shakable-packages):

```javascript
import handleChange from 'gnar-edge/handleChange';
```

or, using the [ES6 tree-shakable module](#es6-tree-shakable-modules):

```javascript
import handleChange from 'gnar-edge/es/handleChange';
```

Read the [package usage](#packages) section if you're unsure of which format to use.

The `handleChange` package provides an `onChange` event handler which updates the state of a bound React element. It accepts an optional callback and an optional set of options.

- `handleChange(<< stateKeyName: String >>, << ?callback: Function >>, << ?options >>)`

**options:**

- `beforeSetState` [Function]: Function to call before updating the state.

It works with:
- `<input>`
- `<input type='checkbox'>`
- `<input type='radio'>`
- `<select>`
- `<textarea>`

**Example:**

```javascript
import React, { Component } from 'react';
import handleChange from 'gnar-edge/handleChange';

export default class MyView extends Component {
  state = {
    firstName: '',
    lastName: ''
  };

  handleChange = this::handleChange;  // when using the ES7 stage 0 bind operator, or
  handleChange = handleChange.bind(this);  // when using the ES5 bind function

  beforeLastNameChange = () => {
    console.log('Before change', this.state.lastName);
  }

  handleLastNameChange = () => {
    console.log('After change', this.state.lastName);
  }

  render() {
    const { firstName, lastName } = this.state;
    const beforeSetState = this.beforeLastNameChange;
    return (
      <div>
        <input onChange={this.handleChange('firstName')} />
        <input onChange={this.handleChange('lastName', this.handleLastNameChange, { beforeSetState })} />
      </div>
      <div>`Hello, ${firstName} ${lastName}!`</div>
    );
  }
}
```

## JWT

The `jwt` package provides a set of utilities to simplify the handling of [jwt tokens](https://jwt.io/).

**Usage:**

Using the [ES5 main package](#main-package):

```javascript
import { jwt } from 'gnar-edge';
const { base64, getJwt, isLoggedIn, jwtDecode } = jwt;  // or use `jwt.base64`, etc.
```

or, using the [ES5 tree-shakable package](#es5-tree-shakable-packages):

```javascript
import { base64, getJwt, isLoggedIn, jwtDecode } from 'gnar-edge/jwt';
```

or, using the [ES6 tree-shakable module](#es6-tree-shakable-modules):

```javascript
import { base64, getJwt, isLoggedIn, jwtDecode } from 'gnar-edge/es/jwt';
```

Read the [package usage](#packages) section if you're unsure of which format to use.

### Base64

The [base64](#base64) package is included in the `jwt` package.

### GetJwt

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/getJwt.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/__tests__/getJwt.test.js)

Retrieves a jwt token from localStorage and decodes it:
- `getJwt(<< keyName: String >>)`. `keyName` defaults to `'jwt'`.

**Example:**

```javascript
import { getJwt } from 'gnar-edge/jwt';

const myJwt = getJwt();
const myCustomKeyJwt = getJwt('J-W-T');
```

### IsLoggedIn

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/isLoggedIn.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/__tests__/isLoggedIn.test.js)

Retrieves a jwt token from localStorage (using [getJwt](#getjwt)) and returns a Boolean indicating whether or not the jwt token has expired:
- `isLoggedIn(<< keyName: String >>)`. `keyName` defaults to `'jwt'`.

**Example:**

```javascript
import { isLoggedIn } from 'gnar-edge/jwt';

...

<Route render={() => <Redirect to={isLoggedIn() ? '/account' : '/login'} />} />
```

### JwtDecode

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/jwtDecode.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/__tests__/jwtDecode.test.js)

Decodes a JWT token.

**Example:**

```javascript
import { jwtDecode } from 'gnar-edge/jwt';

const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiRmxhaXIsIEduYXIgRmxhaXIifQ.-gYkrEvtdghFzzKecKdu_gITvJFwEdOHPYXdp643-2w';

console.log(jwtDecode(jwtToken).name);
```

```bash
>> 'Edge, Gnar Edge'
```

## Notifications

![Notifications Animation](https://gitlab.com/gnaar/edge/raw/master/assets/notifications.gif)

**Usage:**

Using the [ES5 main package](#main-package):

```javascript
import { notifications } from 'gnar-edge';
const {
  ADD_NOTIFICATION,
  DISMISS_NOTIFICATION,
  Notifications,
  dismissNotifications,
  notificationActions,
  notifications,
  notifyError,
  notifyInfo,
  notifySuccess,
  notifyWarning
} = notifications;  // or use `notifications.ADD_NOTIFICATION`, etc.
```

or, using the [ES5 tree-shakable package](#es5-tree-shakable-packages):

```javascript
import {
  ADD_NOTIFICATION,
  DISMISS_NOTIFICATION,
  Notifications,
  dismissNotifications,
  notificationActions,
  notifications,
  notifyError,
  notifyInfo,
  notifySuccess,
  notifyWarning
} from 'gnar-edge/notifications'
```

or, using the [ES6 tree-shakable module](#es6-tree-shakable-modules):

```javascript
import {
  ADD_NOTIFICATION,
  DISMISS_NOTIFICATION,
  Notifications,
  dismissNotifications,
  notificationActions,
  notifications,
  notifyError,
  notifyInfo,
  notifySuccess,
  notifyWarning
} from 'gnar-edge/es/notifications'
```

Read the [package usage](#packages) section if you're unsure of which format to use.

The notifications package requires the following npm packages to be installed in your app (i.e. in the `dependencies` section of `package.json`):
- @material-ui/core
- @material-ui/icons
- animate.css
- classnames
- immutable
- prop-types
- react
- react-dom
- react-redux
- redux
- redux-actions
- redux-saga

If your app is based on [Gnar Powder](https://gitlab.com/gnaar/powder), these packages are already installed. Otherwise, these notifications will work with any Redux Saga-based app that includes the packages listed above. The following command will install any packages you may be missing:

```bash
npm i @material-ui/core @material-ui/icons animate.css classnames immutable prop-types react react-dom react-redux redux redux-actions redux-saga
```

In addition to installing the dependencies, you must add the `Notifications` component to your DOM and add the `notifications` reducer to your root reducer.

### Component

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/components/notifications.jsx)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/__tests__/components/notifications.test.jsx)

The `Notifications` component should be placed at the root of the application, for example:

```javascript
import { Notifications } from 'gnar-edge/notifications';

...

<Grid container>
  <Grid item xs={12}>
    <Switch>
      ...
    </Switch>
    <Notifications />
  </Grid>
</Grid>
```

The component accepts one property, `position`, in the form `'<< vertical position >> << horizontal position >>'` with default `'top right'`. The acceptable values are:
- **vertical:** 'top' or 'bottom'
- **horizontal:** 'left', 'center', or 'right'

### Reducer

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/redux/reducers.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/__tests__/redux/actions-reducers.test.js)

The `notifications` reducer must be added to your root reducer, for example:

```javascript
import { combineReducers } from 'redux';
import { notifications } from 'gnar-edge/notifications';

export default combineReducers({
  ...
  notifications,
  ...
});
```

### Action Types

`ADD_NOTIFICATION` and `DISMISS_NOTIFICATION` are provided for use with an action watcher (optional).

### Actions

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/redux/actions.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/__tests__/redux/actions-reducers.test.js)

The `notificationActions` can be used in any view, for example:

```javascript
import { connect } from 'react-redux';
import { notificationActions } from 'gnar-edge/notifications';
import Button from '@material-ui/core/Button';
import React, { Component } from 'react';

const mapStateToProps = () => ({});

const mapDispatchToProps = notificationActions;

@connect(mapStateToProps, mapDispatchToProps)
export default class MyView extends Component {

  newSuccess = () => { this.props.notifySuccess('More Success!', { autoDismissMillis: 2000, onDismiss: this.newSuccess }); }

  render() {
    const { dismissNotification, notifyError, notifyInfo, notifySuccess, notifyWarning } = this.props;
    return (
      <div>
        <Button onClick={() => notifySuccess('Such Success!')}>Success</Button>
        <Button onClick={() => notifyError('You shall not pass.')}>Error</Button>
        <Button onClick={() => notifyInfo('Gnarly info, dude.')}>Info</Button>
        <Button onClick={() => notifyWarning('Danger, Will Robinson!')}>Warning</Button>
        <Button onClick={() => notifyInfo("I'm sticking around", { key: 'sticky', autoDismissMillis: 0 })}>Sticky</Button>
        <Button onClick={() => dismissNotification('sticky')}>Dismiss Sticky</Button>
        <Button onClick={this.newSuccess}>Perpetual Success</Button>
      </div>
    );
  }
}
```

Each notify method accepts an optional set of options. The available options are:
- **autoDismissMillis**: Number of milliseconds to wait before auto-dismissing the notification; specify 0 for no auto-dismiss (i.e. the user must click the close icon).
- **key**: String to override the autogenerated notification key; for use with an action watcher - when a notification is dismissed, the `DISMISS_NOTIFICATION` action is dispatched with a payload containing the notification key.
- **onDismiss**: Callback to execute when the notification is dismissed; the callback receives a single Boolean parameter which indicates whether or not the notification was dismissed by the user (i.e. the user clicked the close icon).

### Sagas

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/redux/sagas.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/notifications/__tests__/redux/sagas.test.js)

The notifications utility generator functions can be used in any Redux Saga, for example:

```javascript
import { takeEvery } from 'redux-saga/effects';
import { notifySuccess } from 'gnar-edge/notifications';

function* successAction() {
  yield notifySuccess('Such Saga Success!');
}

export default function* watchSuccessAction() {
  yield takeEvery('SUCCESS_ACTION', successAction);
}
```

The available sagas are `dismissNotification`, `notifyError`, `notifyInfo`, `notifySuccess`, `notifyWarning`.

The notify sagas accept the same options (`autoDismissMillis`, `key`, and `onDismiss`) as the notify actions.

## Redux

Boilerplate-nixing convenience functions for creating actions and reducers.

**Usage:**

Using the [ES5 main package](#main-package):

```javascript
import { redux } from 'gnar-edge';
const { gnarActions, gnarReducers } = redux;  // or use `redux.gnarActions`, etc.
```

or, using the [ES5 tree-shakable package](#es5-tree-shakable-packages):

```javascript
import { gnarActions, gnarReducers } from 'gnar-edge/redux'
```

or, using the [ES6 tree-shakable module](#es6-tree-shakable-modules):

```javascript
import { gnarActions, gnarReducers } from 'gnar-edge/es/redux'
```

Read the [package usage](#packages) section if you're unsure of which format to use.


### gnarActions

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/redux/gnarActions.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/redux/__tests__/gnarActions.test.js)

A common pattern when creating Redux actions looks like this:

```javascript
import { createAction } from 'redux-actions';

export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const REALLY_BASIC_ACTION = 'REALLY_BASIC_ACTION';
export const ACTION_WITH_CUSTOM_PAYLOAD_CREATOR = 'ACTION_WITH_CUSTOM_PAYLOAD_CREATOR';

export default {
  groupOfActions: {
    someAction: createAction(SOME_ACTION, (param1, param2, param3) => ({ param1, param2, param3 })),
    someOtherAction: createAction(SOME_OTHER_ACTION, param1 => ({ param1 }))
  },
  someOtherGroupOfActions: {
    reallyBasicAction: createAction(REALLY_BASIC_ACTION, () => ({})),
    actionWithCustomPayloadCreator: createAction(ACTION_WITH_CUSTOM_PAYLOAD_CREATOR, cost => ({ cost: 2 * cost }))
  }
};
```

The actions are often split out into a bunch of small files, like I did with [Gnar Powder before I wrote `gnarActions`](https://gitlab.com/gnaar/powder/tree/b76d0b41170f6e575b0f8be599a99ee9421ec835/src/js/redux/actions).

It would be nice if we could reduce this code a bit. Using `gnarActions`, the code above becomes:

```javascript
import { gnarActions } from 'gnar-edge/es/redux';

export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const REALLY_BASIC_ACTION = 'REALLY_BASIC_ACTION';
export const ACTION_WITH_CUSTOM_PAYLOAD_CREATOR = 'ACTION_WITH_CUSTOM_PAYLOAD_CREATOR';

export default gnarActions({
  groupOfActions: {
    [SOME_ACTION]: [ 'param1', 'param2', 'param3' ],
    [SOME_OTHER_ACTION]: 'param1'
  },
  someOtherGroupOfActions: {
    [REALLY_BASIC_ACTION]: [],
    [ACTION_WITH_CUSTOM_PAYLOAD_CREATOR]: cost => ({ cost: 2 * cost })
  }
});
```

Removing all the boilerplate has a nice impact on our code's readability. It also becomes clear that consolidating actions into fewer files improves maintainability. Check out [the difference in Gnar Powder](https://gitlab.com/gnaar/powder/blob/095ff44307bf8d53fd647fd70714be3e948f9459/src/js/redux/actions.js) after incorporating Gnar Edge Redux.

**Tip**: You might be tempted to use `SOME_ACTION` instead of `[SOME_ACTION]` in the actions object - don't. The interpolated version binds the object key to the action constant.

#### How does `gnarActions` work?

It recursively iterates through the input object looking for the following pattern:

- *key*: All uppercase letters, digits and underscores, i.e. matches `/^([A-Z\d]+_)*[A-Z\d]+$/`
- *value*: String, empty array, array of strings, or function.

For every match, it performs the following transformation:

- *key*: Converts to camelcase
- *value*: Converts to a Redux action following the pseudocode template:

  ```javascript
  createAction(<< key >>, (param1, param2, param3, ...) => ({ param1, param2, param3, ... }))
  ```

  or with a predefined `payloadCreator`:

  ```javascript
  createAction(<< key >>, payloadCreator)
  ```

If a node in the input object doesn't match the key, value pattern outline above, the node is retained unchanged in the output actions (unless the nodes is a literal object, then we recurse through it). This allows us to include actions that don't match the pattern that is transformed using the template. For example:

```javascript
   groupOfActions: {
     [SOME_ACTION]: [ 'param1', 'param2', 'param3' ],
     anotherAction: createAction(SOME_OTHER_ACTION, (param1, param2, ...) => { return someFancyObject; })
   }
```

### gnarReducers

- [Code](https://gitlab.com/gnaar/edge/blob/master/src/modules/redux/gnarReducers.js)
- [Tests / Examples](https://gitlab.com/gnaar/edge/blob/master/src/modules/redux/__tests__/gnarReducers.test.js)

A common pattern when creating Redux reducers looks like:

```javascript
import { handleActions } from 'redux-actions';

export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const YET_ANOTHER_ACTION = 'YET_ANOTHER_ACTION';

export default {
  someStoreNode: handleActions({
    [SOME_ACTION]: (state, { payload }) => ({ ...state, ...payload }),
    [SOME_OTHER_ACTION]: (state, { payload }) => ({ ...state, ...payload }),
  }, {} /* <- initial state */),
  someParentStoreNode: {
    someChildStoreNode: handleActions({
      [YET_ANOTHER_ACTION]: (state, { payload }) => { return someFancyObject; }
    }, { fruit: 'apple' })
  }
};
```

The reducers are often split out into a bunch of small files, like I did with [Gnar Powder before I wrote `gnarReducers`](https://gitlab.com/gnaar/powder/tree/b76d0b41170f6e575b0f8be599a99ee9421ec835/src/js/redux/reducers).

It would be nice if we could also, *ahem*, reduce this code a bit. Using `gnarReducers`, the code above becomes:

```javascript
import { gnarReducers } from 'gnar-edge/es/redux';

export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const YET_ANOTHER_ACTION = 'YET_ANOTHER_ACTION';

export default gnarReducers({
  someStoreNode: {
    basicReducers: [ SOME_ACTION, SOME_OTHER_ACTION ]
  },
  someParentStoreNode: {
    someChildStoreNode: {
      initialState: { fruit: 'apple' },
      customReducers: {
        [YET_ANOTHER_ACTION]: (state, { payload }) => { return someFancyObject; }
      }
    }
  }
});
```

Again, removing all the boilerplate has a nice impact on our code's readability and it becomes clear that consolidating reducers into fewer files improves maintainability. Check out [the difference in Gnar Powder](https://gitlab.com/gnaar/powder/blob/095ff44307bf8d53fd647fd70714be3e948f9459/src/js/redux/reducers.js) after incorporating Gnar Edge Redux.

But wait, there's more! If a node is a string or an array, it's interpreted as basicReducers. That means our example can be further simplified to:

```javascript
export default gnarReducers({
  someStoreNode: [ SOME_ACTION, SOME_OTHER_ACTION ],
  someParentStoreNode: { ... }
});
```

#### How does `gnarReducers` work?

It recursively iterates through the input object looking for the following pattern:

- *key*: The key is not checked
- *value*: String, array, or an object containing either `basicReducers` or `customReducers` (or both)

For every match, it performs the following transformation:

- *key*: No change
- *value*: Converts to a redux reducer following the pseudocode template:

  ```javascript
  const node = << node value is String || Array >> ? { basicReducers: << node value >> } : << node value >>;
  const { initialState, basicReducers, customReducers } = node;
  const parsedReducers = {
    ...(typeof basicReducers === 'string' ? [ basicReducers ] : basicReducers || []).reduce((memo, key) => {
      memo[key] = (state, { payload }) => ({ ...state, ...payload })
      return memo;
    }, {}),
    ...(customReducers || {})
  };
  return handleActions(parsedReducers, initialState || {});
  ```

If a node in the input object isn't a string, an array, or an object that includes `basicReducers` or `customReducers` in its value, the node is retained unchanged in the output reducer (unless the nodes is a literal object, then we recurse through it). This allows us to include predefined reducers in the input object.

##### Optional Parameters

`gnarReducers` accepts two optional parameters, `defaultInitialState`, and `defaultReducer`.

- `defaultInitialState`: Function which returns the desired [`defaultState` parameter](https://redux-actions.js.org/api/handleaction#handleactionsreducermap-defaultstate) for `handleActions`.
- `defaultReducer`: Reducer function to use in place of the reducer used for the basic reducers, i.e.

  ```javascript
  (state, { payload }) => ({ ...state, ...payload })
  ```

##### Immutable Maps

`gnarReducers` is also designed to work with [Immutable Maps](http://facebook.github.io/immutable-js/docs/#/Map).

To activate the Immutable Map mode, either specify `Map` as the `defaultInitialState` or specify a function that returns a `Map`, for example:

```javascript
import { Map } from 'immutable';
import { gnarReducers } from 'gnar-edge/es/redux';

export default gnarReducers({ ... }, Map);
```

In Immutable Map mode, the `defaultReducer` becomes:

```javascript
(state, { payload }) => state.merge(payload)
```

---

<p><div align="center">Made with <img alt='Love' width='32' height='27' src='https://s3-us-west-2.amazonaws.com/project-gnar/heart-32x27.png'> by <a href='https://www.linkedin.com/in/briengivens'>Brien Givens</a></div></p>
