diff --git a/.gitignore b/.gitignore index f9ed950..cd4a2c4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,3 @@ npm-debug.log Session.vim .netrwhist *~ - -lib \ No newline at end of file diff --git a/README.md b/README.md index 1e0a4e5..7ea1211 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ function mapActionToProps(dispatch) { data: { todo } }) } - } + }; } export default connect(mapStateToProps, mapActionToProps)(App); @@ -141,3 +141,10 @@ const mapDispatchToProps = (dispatch) => ({}) export default connect(mapStateToProps, mapDispatchToProps)(Comp) ``` + +## connect([mapStateToProps], [mapDispatchToProps], [mergeProps]) +Connects a Vue component to a Redux store. +### Arguments +* [mapStateToProps(state, [ownAttrs]): stateProps] (__Function__) Subscribes component to Redux store updates. This means that any time the store is updated, mapStateToProps will be called. The results of `mapStateToProps` must be a plain object, which will be merged into the component’s props. +* [mapDispatchToProps(dispatch): dispatchProps] (__Function__) Result must be a plain object +* [mergeProps(stateProps, dispatchProps): props] (__Function__) If specified, it is passed the result of `mapStateToProps()` and `mapDispatchToProps()`. The plain object you return from it will be passed as props to the wrapped component. diff --git a/lib/connect.js b/lib/connect.js new file mode 100644 index 0000000..17b048e --- /dev/null +++ b/lib/connect.js @@ -0,0 +1,144 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +exports.default = connect; + +var _normalizeProps = require('./normalizeProps'); + +var _normalizeProps2 = _interopRequireDefault(_normalizeProps); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function noop() {} + +function getStore(component) { + return component.$store; +} + +function getAttrs(component) { + var attrs = component._self.$options._parentVnode.data.attrs; + if (!attrs) { + return attrs; + } + // Convert props from kebab-case to camelCase notation + return Object.keys(attrs).reduce(function (memo, key) { + return _extends({}, memo, _defineProperty({}, key.replace(/[-](.)/g, function (match, group) { + return group.toUpperCase(); + }), attrs[key])); + }, {}); +} + +function getStates(component, mapStateToProps) { + var store = getStore(component); + var attrs = getAttrs(component); + + return mapStateToProps(store.getState(), attrs) || {}; +} + +function getActions(component, mapActionsToProps) { + var store = getStore(component); + + return mapActionsToProps(store.dispatch, getAttrs.bind(null, component)) || {}; +} + +function getProps(component) { + var props = {}; + var attrs = getAttrs(component); + var propNames = component.vuaReduxPropNames; + + for (var ii = 0; ii < propNames.length; ii++) { + props[propNames[ii]] = component[propNames[ii]]; + } + + return _extends({}, props, attrs); +} + +function getSlots(component) { + return Object.keys(component.$slots).reduce(function (memo, name) { + return _extends({}, memo, _defineProperty({}, name, function () { + return component.$slots[name]; + })); + }, {}); +} + +function defaultMergeProps(stateProps, actionsProps) { + return _extends({}, stateProps, actionsProps); +} + +/** + * 1. utilities are moved above because vue stores methods, states and props + * in the same namespace + * 2. actions are set while created + */ + +/** + * @param mapStateToProps + * @param mapActionsToProps + * @param mergeProps + * @returns Object + */ +function connect(mapStateToProps, mapActionsToProps, mergeProps) { + mapStateToProps = mapStateToProps || noop; + mapActionsToProps = mapActionsToProps || noop; + mergeProps = mergeProps || defaultMergeProps; + + return function (children) { + + /** @namespace children.collect */ + if (children.collect) { + children.props = _extends({}, (0, _normalizeProps2.default)(children.props || {}), (0, _normalizeProps2.default)(children.collect || {})); + + var msg = 'vua-redux: collect is deprecated, use props ' + ('in ' + (children.name || 'anonymous') + ' component'); + + console.warn(msg); + } + + return { + name: 'ConnectVuaRedux-' + (children.name || 'children'), + + render: function render(h) { + return h(children, { + props: getProps(this), + scopedSlots: getSlots(this) + }); + }, + data: function data() { + var state = getStates(this, mapStateToProps); + var actions = getActions(this, mapActionsToProps); + var merged = mergeProps(state, actions); + var propNames = Object.keys(merged); + + return _extends({}, merged, { + vuaReduxPropNames: propNames + }); + }, + created: function created() { + var _this = this; + + var store = getStore(this); + + this.vuaReduxUnsubscribe = store.subscribe(function () { + var state = getStates(_this, mapStateToProps); + var actions = getActions(_this, mapActionsToProps); + var merged = mergeProps(state, actions); + var propNames = Object.keys(merged); + _this.vuaReduxPropNames = propNames; + + for (var ii = 0; ii < propNames.length; ii++) { + _this[propNames[ii]] = merged[propNames[ii]]; + } + }); + }, + beforeDestroy: function beforeDestroy() { + this.vuaReduxUnsubscribe(); + } + }; + }; +} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..2c1e171 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,19 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.reduxStorePlugin = exports.connect = undefined; + +var _connect2 = require('./connect'); + +var _connect3 = _interopRequireDefault(_connect2); + +var _reduxStorePlugin2 = require('./reduxStorePlugin'); + +var _reduxStorePlugin3 = _interopRequireDefault(_reduxStorePlugin2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.connect = _connect3.default; +exports.reduxStorePlugin = _reduxStorePlugin3.default; \ No newline at end of file diff --git a/lib/normalizeProps.js b/lib/normalizeProps.js new file mode 100644 index 0000000..28c3664 --- /dev/null +++ b/lib/normalizeProps.js @@ -0,0 +1,48 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = normalizeProps; + +var _isArray = require('lodash/isArray'); + +var _isArray2 = _interopRequireDefault(_isArray); + +var _isPlainObject = require('lodash/isPlainObject'); + +var _isPlainObject2 = _interopRequireDefault(_isPlainObject); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// https://github.com/vuejs/vue/blob/dev/src/util/options.js +function normalizeProps(props) { + var i, + val, + normalizedProps = {}; + + if ((0, _isArray2.default)(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + normalizedProps[val] = null; + } else if (val.name) { + normalizedProps[val.name] = val; + } + } + } else if ((0, _isPlainObject2.default)(props)) { + var keys = Object.keys(props); + i = keys.length; + while (i--) { + var key = keys[i]; + val = props[key]; + normalizedProps[key] = props[key]; + if (typeof val === 'function') { + normalizedProps[key] = { type: val }; + } + } + } + + return normalizedProps; +} \ No newline at end of file diff --git a/lib/normalizeProps.spec.js b/lib/normalizeProps.spec.js new file mode 100644 index 0000000..4e97fe5 --- /dev/null +++ b/lib/normalizeProps.spec.js @@ -0,0 +1,22 @@ +'use strict'; + +var _expect = require('expect'); + +var _expect2 = _interopRequireDefault(_expect); + +var _normalizeProps = require('./normalizeProps'); + +var _normalizeProps2 = _interopRequireDefault(_normalizeProps); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe('normalize props', function () { + it('should normalize array props', function () { + (0, _expect2.default)((0, _normalizeProps2.default)(['a', 'b'])).toEqual({ a: null, b: null }); + }); + + it('should normalize object props', function () { + var props = { 'a': { type: String }, 'b': null }; + (0, _expect2.default)((0, _normalizeProps2.default)(props)).toEqual(props); + }); +}); \ No newline at end of file diff --git a/lib/reduxStorePlugin.js b/lib/reduxStorePlugin.js new file mode 100644 index 0000000..53621ca --- /dev/null +++ b/lib/reduxStorePlugin.js @@ -0,0 +1,21 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = reduxStorePlugin; +function reduxStorePlugin(Vue) { + var storeId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'store'; + + Vue.mixin({ + beforeCreate: function beforeCreate() { + var options = this.$options; + // store injection + if (options[storeId]) { + this.$store = options.store; + } else if (options.parent && options.parent.$store) { + this.$store = options.parent.$store; + } + } + }); +} \ No newline at end of file diff --git a/package.json b/package.json index 69093f1..1ba7870 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-vue", - "version": "0.7.1", + "version": "0.8.1", "description": "Vue Redux binding higher order component", "author": "Nadim Tuhin", "repository": { diff --git a/src/connect.js b/src/connect.js index d180edd..108f826 100644 --- a/src/connect.js +++ b/src/connect.js @@ -8,7 +8,15 @@ function getStore(component) { } function getAttrs(component) { - return component._self.$options._parentVnode.data.attrs; + const attrs = component._self.$options._parentVnode.data.attrs; + if (!attrs) { + return attrs + } + // Convert props from kebab-case to camelCase notation + return Object.keys(attrs).reduce((memo, key) => ({ + ...memo, + [key.replace(/[-](.)/g, (match, group) => group.toUpperCase())]: attrs[key], + }), {}) } function getStates(component, mapStateToProps) { @@ -27,15 +35,10 @@ function getActions(component, mapActionsToProps) { function getProps(component) { let props = {}; const attrs = getAttrs(component); - const stateNames = component.vuaReduxStateNames; - const actionNames = component.vuaReduxActionNames; - - for (let ii = 0; ii < stateNames.length; ii++) { - props[stateNames[ii]] = component[stateNames[ii]]; - } + const propNames = component.vuaReduxPropNames; - for (let ii = 0; ii < actionNames.length; ii++) { - props[actionNames[ii]] = component[actionNames[ii]]; + for (let ii = 0; ii < propNames.length; ii++) { + props[propNames[ii]] = component[propNames[ii]]; } return { @@ -44,6 +47,20 @@ function getProps(component) { }; } +function getSlots(component) { + return Object.keys(component.$slots).reduce((memo, name) => ({ + ...memo, + [name]: () => component.$slots[name], + }), {}) +} + +function defaultMergeProps(stateProps, actionsProps) { + return { + ...stateProps, + ...actionsProps, + }; +} + /** * 1. utilities are moved above because vue stores methods, states and props * in the same namespace @@ -53,11 +70,13 @@ function getProps(component) { /** * @param mapStateToProps * @param mapActionsToProps + * @param mergeProps * @returns Object */ -export default function connect(mapStateToProps, mapActionsToProps) { +export default function connect(mapStateToProps, mapActionsToProps, mergeProps) { mapStateToProps = mapStateToProps || noop; mapActionsToProps = mapActionsToProps || noop; + mergeProps = mergeProps || defaultMergeProps; return (children) => { @@ -78,22 +97,21 @@ export default function connect(mapStateToProps, mapActionsToProps) { name: `ConnectVuaRedux-${children.name || 'children'}`, render(h) { - const props = getProps(this); - - return h(children, { props }); + return h(children, { + props: getProps(this), + scopedSlots: getSlots(this), + }); }, data() { const state = getStates(this, mapStateToProps); const actions = getActions(this, mapActionsToProps); - const stateNames = Object.keys(state); - const actionNames = Object.keys(actions); + const merged = mergeProps(state, actions); + const propNames = Object.keys(merged); return { - ...state, - ...actions, - vuaReduxStateNames: stateNames, - vuaReduxActionNames: actionNames + ...merged, + vuaReduxPropNames: propNames, }; }, @@ -102,11 +120,13 @@ export default function connect(mapStateToProps, mapActionsToProps) { this.vuaReduxUnsubscribe = store.subscribe(() => { const state = getStates(this, mapStateToProps); - const stateNames = Object.keys(state); - this.vuaReduxStateNames = stateNames; + const actions = getActions(this, mapActionsToProps); + const merged = mergeProps(state, actions); + const propNames = Object.keys(merged); + this.vuaReduxPropNames = propNames; - for (let ii = 0; ii < stateNames.length; ii++) { - this[stateNames[ii]] = state[stateNames[ii]]; + for (let ii = 0; ii < propNames.length; ii++) { + this[propNames[ii]] = merged[propNames[ii]]; } }); },