<!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()]] — <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();
},
};
};
});