<!doctype html>
<html>

<head>
  <title>Polymer Redux Binding</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">
  <script href="https://raw.githubusercontent.com/Download/polymer-cdn/master/lib/webcomponentsjs/webcomponents.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.0.4/redux.js"></script>
  <script src="polymer-redux.js"></script>

  <link rel="import" href="https://cdn.rawgit.com/download/polymer-cdn/1.5.0/lib/iron-demo-helpers/demo-pages-shared-styles.html">
  <link rel="import" href="https://cdn.rawgit.com/download/polymer-cdn/1.5.0/lib/iron-demo-helpers/demo-snippet.html">
</head>

<body>

  <div class="vertical-section-container">
    <h3>Polymer Redux Demo</h3>
    <demo-snippet>
      <template>
        <!-- redux setup -->
        <script>
          const reducer = (state, action) => {
            if (!state) return {
              contacts: {
                friends: [
                  'F1',
                  'F2',
                  'F3'
                ]
              }
            };

            // copy friends list
            friends = state.contacts.friends.slice(0);

            switch (action.type) {
              case 'ADD_FRIEND':
                friends.push(action.friend);
                break;
              case 'REMOVE_FRIEND':
                const idx = friends.indexOf(action.friend)
                if (idx !== -1) {
                  friends.splice(idx, 1)
                }
                break;
              case 'SORT_FRIENDS':
                friends.sort()
                break;
            }

            state.contacts = { friends: friends };

            return state;
          }
          const store = Redux.createStore(reducer);
          const ReduxBehavior = PolymerRedux(store);
        </script>

        <!-- friends list module -->
        <dom-module id="friends-list">
          <template>
            <p>
              <span>You have [[friends.length]] friend(s).</span>
              <template is="dom-if" if="[[canSortFriends(contacts.friends.length)]]">
                <button on-click="sortFriends">Sort Friends</button>
              </template>
            </p>
            <ul>
              <template is="dom-repeat" items="[[contacts.friends]]">
                <li>
                  <friends-friend friend="[[item]]">
                    <span>[[item]]</span>
                    <button on-click="removeFriend">Remove</button>
                  </friends-friend>
                </li>
              </template>
            </ul>
            <input id="friend-name" placeholder="Name" on-keypress="handleKeypress">
            <button on-click="addFriend">Add Friend</button>
          </template>
        </dom-module>
        <script>
          Polymer({
            is: 'friends-list',
            behaviors: [ReduxBehavior],
            properties: {
              contacts: {
                type: Object,
                statePath: 'contacts'
              }
            },
            actions: {
              add: function(name) {
                return {
                  type: 'ADD_FRIEND',
                  friend: name
                };
              },
              remove: function(name) {
                return {
                  type: 'REMOVE_FRIEND',
                  friend: name
                };
              },
              sort: function() {
                return {
                  type: 'SORT_FRIENDS'
                };
              },
            },
            addFriend: function() {
              const input = this.$['friend-name'];
              if (input.value) {
                this.dispatch('add', input.value);
                input.value = '';
                input.focus();
              }
            },
            removeFriend: function(event) {
              this.dispatch('remove', event.model.item);
            },
            sortFriends: function() {
              this.dispatch('sort');
            },
            canSortFriends: function(length) {
              return length > 1;
            },
            handleKeypress: function(event) {
              if (event.charCode === 13) {
                this.addFriend();
              }
            }
          });
        </script>

        <dom-module id='friends-friend'>
          <template>
            <div>template instance: [[_instanceNum()]] &mdash; <content></content></div>
          </template>
        </dom-module>

        <script>
          var instances = 0;

          Polymer({
            is: 'friends-friend',

            properties: {
              instanceNum: {
                type: Number,
                value: 0
              },
              friend: {
                type: Object,
                observer: '_watchConfig'
              }
            },

            created() {
              this.set('instanceNum', ++instances);
              this.fire('log', 'template', this.instanceNum, 'created');
            },

            attached() {
              this.fire('log', 'template', this.instanceNum, 'attached');
            },

            detached() {
              this.fire('log', 'template', this.instanceNum, 'detached');
            },

            _watchConfig(val, oldVal) {
              this.fire('log', 'template ' + this.instanceNum + ' receives friend ' + val);
            },

            _instanceNum() {
              return this.instanceNum;
            }
          });
        </script>

        <!-- demo -->
        <friends-list></friends-list>
      </template>
    </demo-snippet>
  </div>
</body>

</html>
(function(root, factory) {
    /* istanbul ignore next */
    if (typeof exports === 'object' && typeof module === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {
        root['PolymerRedux'] = factory();
    }
})(this, function() {
    var warning = 'Polymer Redux: <%s>.%s has "notify" enabled, two-way bindings goes against Redux\'s paradigm';

    /**
     * Factory function for creating a listener for a give Polymer element. The
     * returning listener should be passed to `store.subscribe`.
     *
     * @param {HTMLElement} element Polymer element.
     * @return {Function} Redux subcribe listener.
     */
    function createListener(element, store) {
        var props = [];

        // property bindings
        if (element.properties != null) {
            Object.keys(element.properties).forEach(function(name) {
                prop = element.properties[name];
                if (prop.hasOwnProperty('statePath')) {
                    // notify flag, warn against two-way bindings
                    if (prop.notify && !prop.readOnly) {
                        console.warn(warning, element.is, name);
                    }
                    props.push({
                        name: name,
                        // Empty statePath return state
                        path: prop.statePath || store.getState,
                        readOnly: prop.readOnly,
                        type: prop.type
                    });
                }
            });
        }

        // redux listener
        return function() {
            var state = store.getState();
            props.forEach(function(property) {
                var propName = property.name;
                var splices = [];
                var value, previous;

                // statePath, a path or function.
                var path = property.path;
                if (typeof path == 'function') {
                    value = path.call(element, state);
                } else {
                    value = Polymer.Base.get(path, state);
                }

                // prevent unnecesary polymer notifications
                previous = element.get(property.name);
                if (value === previous) {
                    return;
                }

                // type of array, work out splices before setting the value
                if (property.type === Array) {
                    value = value || /* istanbul ignore next */ [];
                    previous = previous || /* istanbul ignore next */ [];

                    // check the value type
                    if (!Array.isArray(value)) {
                        throw new TypeError(
                            '<'+ element.is +'>.'+ propName +' type is Array but given: ' + (typeof value)
                        );
                    }

                    splices = Polymer.ArraySplice.calculateSplices(value, previous);
                }

                // set
                if (property.readOnly) {
                    element.notifyPath(propName, value);
                } else {
                    element.set(propName, value);
                }

                // notify element of splices
                if (splices.length) {
                    element.notifySplices(propName, splices);
                }
            });
            element.fire('state-changed', state);
        }
    }

    /**
     * Binds an given Polymer element to a Redux store.
     *
     * @param {HTMLElement} element Polymer element.
     * @param {Object} store Redux store.
     */
    function bindReduxListener(element, store) {
        var listener;

        if (element._reduxUnsubscribe) return;

        listener = createListener(element, store);
        listener(); // start bindings

        element._reduxUnsubscribe = store.subscribe(listener);
    }

    /**
     * Unbinds a Polymer element from a Redux store.
     *
     * @param {HTMLElement} element
     */
    function unbindReduxListener(element) {
        if (typeof element._reduxUnsubscribe === 'function') {
            element._reduxUnsubscribe();
            delete element._reduxUnsubscribe;
        }
    }

    /**
     * Dispatches a Redux action via a Polymer element. This gives the element
     * a polymorphic dispatch function. See the readme for the various ways to
     * dispatch.
     *
     * @param {HTMLElement} element Polymer element.
     * @param {Object} store Redux store.
     * @param {Array} args The arguments passed to `element.dispatch`.
     * @return {Object} The computed Redux action.
     */
    function dispatchReduxAction(element, store, args) {
        var action = args[0];
        var actions = element.actions;

        // action name
        if (actions && typeof action === 'string') {
            if (typeof actions[action] !== 'function') {
                throw new TypeError('Polymer Redux: <' + element.is + '> has no action "' + action + '"');
            }
            return store.dispatch(actions[action].apply(element, args.slice(1)));
        }

        // action creator
        if (typeof action === 'function' && action.length === 0) {
            return store.dispatch(action());
        }

        // action
        return store.dispatch(action);
    }

    /**
     * Creates PolymerRedux behaviors from a given Redux store.
     *
     * @param {Object} store Redux store.
     * @return {PolymerRedux}
     */
    return function(store) {
        var PolymerRedux;

        // check for store
        if (!store) {
            throw new TypeError('missing redux store');
        }

        /**
         * `PolymerRedux` binds a given Redux store's state to implementing Elements.
         *
         * Full documentation available, https://github.com/tur-nr/polymer-redux.
         *
         * @polymerBehavior PolymerRedux
         * @demo demo/index.html
         */
        return PolymerRedux = {
            /**
             * Fired when the Redux store state changes.
             * @event state-changed
             * @param {*} state
             */

            ready: function() {
                bindReduxListener(this, store);
            },

            attached: function() {
                bindReduxListener(this, store);
            },

            detached: function() {
                unbindReduxListener(this);
            },

            /**
             * Dispatches an action to the Redux store.
             *
             * @param {String|Object|Function} action
             * @return {Object} The action that was dispatched.
             */
            dispatch: function(action /*, [...args] */) {
                var args = Array.prototype.slice.call(arguments);
                return dispatchReduxAction(this, store, args);
            },

            /**
             * Gets the current state in the Redux store.
             * @return {*}
             */
            getState: function() {
                return store.getState();
            },
        };
    };
});