, {}> {
- render(): React.ReactNode;
- }
-
- interface ComponentConstructor {
- new (props: P, context?: any): React.Component
;
- }
-}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..b47eb06
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,44 @@
+import fs from 'fs';
+import buble from 'rollup-plugin-buble';
+import uglify from 'rollup-plugin-uglify';
+import replace from 'rollup-plugin-post-replace';
+
+let pkg = JSON.parse(fs.readFileSync('./package.json'));
+
+let format = process.env.FORMAT;
+
+export default {
+ strict: false,
+ sourcemap: true,
+ exports: 'default',
+ input: pkg.source,
+ output: {
+ format,
+ name: pkg.amdName || pkg.name,
+ file: (format==='es' && pkg.module) || (format==='umd' && pkg['umd:main']) || pkg.main
+ },
+ external: ['preact'],
+ globals: {
+ preact: 'preact'
+ },
+ plugins: [
+ buble({
+ objectAssign: 'assign',
+ jsx: 'h'
+ }),
+ format==='cjs' && replace({
+ 'module.exports = index;': '',
+ 'var index =': 'module.exports ='
+ }),
+ format==='umd' && replace({
+ 'return index;': '',
+ 'var index =': 'return'
+ }),
+ format!=='es' && uglify({
+ output: { comments: false },
+ mangle: {
+ toplevel: format==='cjs'
+ }
+ })
+ ]
+};
diff --git a/src/combined/preact.js b/src/combined/preact.js
deleted file mode 100644
index 67e8924..0000000
--- a/src/combined/preact.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as createStore } from '../index';
-export { Provider, connect } from '../integrations/preact';
\ No newline at end of file
diff --git a/src/combined/react.js b/src/combined/react.js
deleted file mode 100644
index c5e4bdc..0000000
--- a/src/combined/react.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as createStore } from '../index';
-export { Provider, connect } from '../integrations/react';
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 4df2006..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { assign } from './util';
-
-/**
- * Creates a new store, which is a tiny evented state container.
- * @name createStore
- * @param {Object} [state={}] Optional initial state
- * @returns {store}
- * @example
- * let store = createStore();
- * store.subscribe( state => console.log(state) );
- * store.setState({ a: 'b' }); // logs { a: 'b' }
- * store.setState({ c: 'd' }); // logs { a: 'b', c: 'd' }
- */
-export default function createStore(state) {
- let listeners = [];
- state = state || {};
-
- function unsubscribe(listener) {
- let out = [];
- for (let i=0; i stateUpdate`
- * @returns {Function} boundAction()
- */
- action(action) {
- function apply(result) {
- setState(result, false, action);
- }
-
- // Note: perf tests verifying this implementation: https://esbench.com/bench/5a295e6299634800a0349500
- return function() {
- let args = [state];
- for (let i=0; i { unsubscribe(listener); };
- },
-
- /**
- * Remove a previously-registered listener function.
- * @param {Function} listener The callback previously passed to `subscribe()` that should be removed.
- * @function
- */
- unsubscribe,
-
- /**
- * Retrieve the current state object.
- * @returns {Object} state
- */
- getState() {
- return state;
- }
- };
-}
diff --git a/src/integrations/preact.js b/src/integrations/preact.js
deleted file mode 100644
index 81c09e3..0000000
--- a/src/integrations/preact.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { h, Component } from 'preact';
-import { assign, mapActions, select } from '../util';
-
-/**
- * Wire a component up to the store. Passes state as props, re-renders on change.
- * @param {Function|Array|String} mapStateToProps A function mapping of store state to prop values, or an array/CSV of properties to map.
- * @param {Function|Object} [actions] Action functions (pure state mappings), or a factory returning them. Every action function gets current state as the first parameter and any other params next
- * @returns {Component} ConnectedComponent
- * @example
- * const Foo = connect('foo,bar')( ({ foo, bar }) =>
)
- * @example
- * const actions = { someAction }
- * const Foo = connect('foo,bar', actions)( ({ foo, bar, someAction }) =>
)
- * @example
- * @connect( state => ({ foo: state.foo, bar: state.bar }) )
- * export class Foo { render({ foo, bar }) { } }
- */
-export function connect(mapStateToProps, actions) {
- if (typeof mapStateToProps!='function') {
- mapStateToProps = select(mapStateToProps || {});
- }
- return Child => {
- function Wrapper(props, context) {
- const store = context.store;
- let state = mapStateToProps(store ? store.getState() : {}, props);
- const boundActions = actions ? mapActions(actions, store) : { store };
- let update = () => {
- let mapped = mapStateToProps(store ? store.getState() : {}, props);
- for (let i in mapped) if (mapped[i]!==state[i]) {
- state = mapped;
- return this.setState({});
- }
- for (let i in state) if (!(i in mapped)) {
- state = mapped;
- return this.setState({});
- }
- };
- this.componentWillReceiveProps = p => {
- props = p;
- update();
- };
- this.componentDidMount = () => {
- store.subscribe(update);
- };
- this.componentWillUnmount = () => {
- store.unsubscribe(update);
- };
- this.render = props => h(Child, assign(assign(assign({}, boundActions), props), state));
- }
- return (Wrapper.prototype = new Component()).constructor = Wrapper;
- };
-}
-
-
-/**
- * Provider exposes a store (passed as `props.store`) into context.
- *
- * Generally, an entire application is wrapped in a single `` at the root.
- * @class
- * @extends Component
- * @param {Object} props
- * @param {Store} props.store A {Store} instance to expose via context.
- */
-export function Provider(props) {
- this.getChildContext = () => ({ store: props.store });
-}
-Provider.prototype.render = props => props.children && props.children[0] || props.children;
diff --git a/src/integrations/react.js b/src/integrations/react.js
deleted file mode 100644
index 672b3db..0000000
--- a/src/integrations/react.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { createElement, Children, Component } from 'react';
-import { assign, mapActions, select } from '../util';
-
-const CONTEXT_TYPES = {
- store: () => {}
-};
-
-/** Wire a component up to the store. Passes state as props, re-renders on change.
- * @param {Function|Array|String} mapStateToProps A function mapping of store state to prop values, or an array/CSV of properties to map.
- * @param {Function|Object} [actions] Action functions (pure state mappings), or a factory returning them. Every action function gets current state as the first parameter and any other params next
- * @returns {Component} ConnectedComponent
- * @example
- * const Foo = connect('foo,bar')( ({ foo, bar }) =>
)
- * @example
- * const actions = { someAction }
- * const Foo = connect('foo,bar', actions)( ({ foo, bar, someAction }) =>
)
- * @example
- * @connect( state => ({ foo: state.foo, bar: state.bar }) )
- * export class Foo { render({ foo, bar }) { } }
- */
-export function connect(mapStateToProps, actions) {
- if (typeof mapStateToProps!=='function') {
- mapStateToProps = select(mapStateToProps || []);
- }
- return Child => {
- function Wrapper(props, context) {
- Component.call(this, props, context);
- const store = context.store;
- let state = mapStateToProps(store ? store.getState() : {}, props);
- const boundActions = actions ? mapActions(actions, store) : { store };
- let update = () => {
- let mapped = mapStateToProps(store ? store.getState() : {}, props);
- for (let i in mapped) if (mapped[i]!==state[i]) {
- state = mapped;
- return this.forceUpdate();
- }
- for (let i in state) if (!(i in mapped)) {
- state = mapped;
- return this.forceUpdate();
- }
- };
- this.UNSAFE_componentWillReceiveProps = p => {
- props = p;
- update();
- };
- this.componentDidMount = () => {
- store.subscribe(update);
- };
- this.componentWillUnmount = () => {
- store.unsubscribe(update);
- };
- this.render = () => createElement(Child, assign(assign(assign({}, boundActions), this.props), state));
- }
- Wrapper.contextTypes = CONTEXT_TYPES;
- return (Wrapper.prototype = Object.create(Component.prototype)).constructor = Wrapper;
- };
-}
-
-
-/** Provider exposes a store (passed as `props.store`) into context.
- *
- * Generally, an entire application is wrapped in a single `` at the root.
- * @class
- * @extends Component
- * @param {Object} props
- * @param {Store} props.store A {Store} instance to expose via context.
- */
-export class Provider extends Component {
- getChildContext() {
- return { store: this.props.store };
- }
- render() {
- return Children.only(this.props.children);
- }
-}
-Provider.childContextTypes = CONTEXT_TYPES;
diff --git a/src/util.js b/src/util.js
deleted file mode 100644
index 7050423..0000000
--- a/src/util.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Bind an object/factory of actions to the store and wrap them.
-export function mapActions(actions, store) {
- if (typeof actions==='function') actions = actions(store);
- let mapped = {};
- for (let i in actions) {
- mapped[i] = store.action(actions[i]);
- }
- return mapped;
-}
-
-
-// select('foo,bar') creates a function of the form: ({ foo, bar }) => ({ foo, bar })
-export function select(properties) {
- if (typeof properties==='string') properties = properties.split(/\s*,\s*/);
- return state => {
- let selected = {};
- for (let i=0; i2;)P.push(arguments[i]);t&&null!=t.children&&(P.length||P.push(t.children),delete t.children);while(P.length)if((o=P.pop())&&void 0!==o.pop)for(i=o.length;i--;)P.push(o[i]);else"boolean"==typeof o&&(o=null),(r="function"!=typeof e)&&(null==o?o="":"number"==typeof o?o+="":"string"!=typeof o&&(r=!1)),r&&n?l[l.length-1]+=o:l===W?l=[o]:l.push(o),n=r;var a=new T;return a.nodeName=e,a.children=l,a.attributes=null==t?void 0:t,a.key=null==t?void 0:t.key,void 0!==M.vnode&&M.vnode(a),a}function t(e,t){for(var n in t)e[n]=t[n];return e}function n(e,t){null!=e&&("function"==typeof e?e(t):e.current=t)}function o(n,o){return e(n.nodeName,t(t({},n.attributes),o),arguments.length>2?[].slice.call(arguments,2):n.children)}function r(e){!e.__d&&(e.__d=!0)&&1==V.push(e)&&(M.debounceRendering||D)(i)}function i(){var e;while(e=V.pop())e.__d&&x(e)}function l(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&a(e,t.nodeName):n||e._componentConstructor===t.nodeName}function a(e,t){return e.__n===t||e.nodeName.toLowerCase()===t.toLowerCase()}function u(e){var n=t({},e.attributes);n.children=e.children;var o=e.nodeName.defaultProps;if(void 0!==o)for(var r in o)void 0===n[r]&&(n[r]=o[r]);return n}function c(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.__n=e,n}function p(e){var t=e.parentNode;t&&t.removeChild(e)}function s(e,t,o,r,i){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n(o,null),n(r,e);else if("class"!==t||i)if("style"===t){if(r&&"string"!=typeof r&&"string"!=typeof o||(e.style.cssText=r||""),r&&"object"==typeof r){if("string"!=typeof o)for(var l in o)l in r||(e.style[l]="");for(var l in r)e.style[l]="number"==typeof r[l]&&!1===E.test(l)?r[l]+"px":r[l]}}else if("dangerouslySetInnerHTML"===t)r&&(e.innerHTML=r.__html||"");else if("o"==t[0]&&"n"==t[1]){var a=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),r?o||e.addEventListener(t,_,a):e.removeEventListener(t,_,a),(e.__l||(e.__l={}))[t]=r}else if("list"!==t&&"type"!==t&&!i&&t in e){try{e[t]=null==r?"":r}catch(e){}null!=r&&!1!==r||"spellcheck"==t||e.removeAttribute(t)}else{var u=i&&t!==(t=t.replace(/^xlink:?/,""));null==r||!1===r?u?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof r&&(u?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),r):e.setAttribute(t,r))}else e.className=r||""}function _(e){return this.__l[e.type](M.event&&M.event(e)||e)}function f(){var e;while(e=A.shift())M.afterMount&&M.afterMount(e),e.componentDidMount&&e.componentDidMount()}function d(e,t,n,o,r,i){H++||(R=null!=r&&void 0!==r.ownerSVGElement,B=null!=e&&!("__preactattr_"in e));var l=h(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--H||(B=!1,i||f()),l}function h(e,t,n,o,r){var i=e,l=R;if(null!=t&&"boolean"!=typeof t||(t=""),"string"==typeof t||"number"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),v(e,!0))),i.__preactattr_=!0,i;var u=t.nodeName;if("function"==typeof u)return N(e,t,n,o);if(R="svg"===u||"foreignObject"!==u&&R,u+="",(!e||!a(e,u))&&(i=c(u,R),e)){while(e.firstChild)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),v(e,!0)}var p=i.firstChild,s=i.__preactattr_,_=t.children;if(null==s){s=i.__preactattr_={};for(var f=i.attributes,d=f.length;d--;)s[f[d].name]=f[d].value}return!B&&_&&1===_.length&&"string"==typeof _[0]&&null!=p&&void 0!==p.splitText&&null==p.nextSibling?p.nodeValue!=_[0]&&(p.nodeValue=_[0]):(_&&_.length||null!=p)&&m(i,_,n,o,B||null!=s.dangerouslySetInnerHTML),y(i,t.attributes,s),R=l,i}function m(e,t,n,o,r){var i,a,u,c,s,_=e.childNodes,f=[],d={},m=0,b=0,y=_.length,g=0,w=t?t.length:0;if(0!==y)for(var C=0;C {
- describe('unistore', () => {
- it('should export only a single function as default', () => {
- expect(createStore).toBeInstanceOf(Function);
- });
- });
-
- describe('unistore/preact', () => {
- it('should export connect', () => {
- expect(preact).toHaveProperty('connect', expect.any(Function));
- });
- it('should export Provider', () => {
- expect(preact).toHaveProperty('Provider', expect.any(Function));
- });
- it('should no export anything else', () => {
- expect(preact).toEqual({
- connect: preact.connect,
- Provider: preact.Provider
- });
- });
- });
-
- describe('smoke test (preact)', () => {
- it('should render', done => {
- const { Provider, connect } = preact;
- const actions = ({ getState, setState }) => ({
- incrementTwice(state) {
- setState({ count: state.count + 1 });
- return new Promise(r =>
- setTimeout(() => {
- r({ count: getState().count + 1 });
- }, 20)
- );
- }
- });
- const App = connect('count', actions)(({ count, incrementTwice }) => (
- count: {count}
- ));
- const store = createStore({ count: 0 });
- let root = document.createElement('div');
- render(
-
-
- ,
- root
- );
- expect(store.getState()).toEqual({ count: 0 });
- root.firstElementChild.click();
- expect(store.getState()).toEqual({ count: 1 });
- setTimeout(() => {
- expect(store.getState()).toEqual({ count: 2 });
- expect(root.textContent).toBe('count: 2');
- done();
- }, 30);
- });
- });
-});
diff --git a/test/preact/preact-8.test.js b/test/preact/preact-8.test.js
deleted file mode 100644
index 654f8ae..0000000
--- a/test/preact/preact-8.test.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// This runs the Preact tests against a copy of Preact 8.4.2.
-jest.isolateModules(() => {
- const preact = require('../fixtures/preact-8.min.js');
- jest.setMock('preact', preact);
-
- // Patch Preact 8's render to work like Preact 10:
- let render = preact.render;
- preact.render = (vnode, parent) => parent._root = render(vnode, parent, parent._root);
-
- global.IS_PREACT_8 = true;
- require('./preact.test');
- delete global.IS_PREACT_8;
-});
diff --git a/test/preact/preact.test.js b/test/preact/preact.test.js
deleted file mode 100644
index a0c1ce9..0000000
--- a/test/preact/preact.test.js
+++ /dev/null
@@ -1,215 +0,0 @@
-import { h, render } from 'preact';
-import createStore from '../../src';
-import { Provider, connect } from '../../src/integrations/preact';
-
-const sleep = ms => new Promise( r => setTimeout(r, ms) );
-
-const NO_CHILDREN = global.IS_PREACT_8 ? expect.anything() : undefined;
-
-describe(`integrations/preact${global.IS_PREACT_8 ? '-8' : ''}`, () => {
- describe('', () => {
- afterEach(() => {
- render(null, document.body);
- });
-
- it('should provide props into context', () => {
- const Child = jest.fn();
-
- render(
-
-
- ,
- document.body
- );
- expect(Child).toHaveBeenCalledWith(expect.anything(), { store: 'a' });
-
- render(null, document.body);
-
- let store = { name: 'obj' };
- render(
-
-
- ,
- document.body
- );
- expect(Child).toHaveBeenCalledWith(expect.anything(), { store });
- });
- });
-
- describe('connect()', () => {
- afterEach(() => {
- render(null, document.body);
- });
-
- it('should pass mapped state as props', () => {
- let state = { a: 'b' };
- const store = { subscribe: jest.fn(), unsubscribe: jest.fn(), getState: () => state };
- const Child = jest.fn();
- const ConnectedChild = connect(Object)(Child);
- render(
-
-
- ,
- document.body
- );
- expect(Child).toHaveBeenCalledWith(
- { a: 'b', store, children: NO_CHILDREN },
- expect.anything()
- );
- expect(store.subscribe).toBeCalled();
- });
-
- it('should transform string selector', () => {
- let state = { a: 'b', b: 'c', c: 'd' };
- const store = { subscribe: jest.fn(), unsubscribe: jest.fn(), getState: () => state };
- const Child = jest.fn();
- const ConnectedChild = connect('a, b')(Child);
- render(
-
-
- ,
- document.body
- );
- expect(Child).toHaveBeenCalledWith(
- { a: 'b', b: 'c', store, children: NO_CHILDREN },
- expect.anything()
- );
- expect(store.subscribe).toBeCalled();
- });
-
- it('should subscribe to store on mount', async () => {
- const store = { subscribe: jest.fn(), unsubscribe: jest.fn(), getState: () => ({}) };
- jest.spyOn(store, 'subscribe');
- const ConnectedChild = connect(Object)(() => null);
-
- render(
-
-
- ,
- document.body
- );
-
- expect(store.subscribe).toBeCalledWith(expect.any(Function));
- });
-
- it('should unsubscribe from store when unmounted', async () => {
- const store = createStore();
- jest.spyOn(store, 'unsubscribe');
- const ConnectedChild = connect(Object)(() => null);
- render(
-
-
- ,
- document.body
- );
- await sleep(1);
- render(null, document.body);
- expect(store.unsubscribe).toBeCalled();
- });
-
- it('should subscribe to store', async () => {
- const store = createStore();
- const Child = jest.fn();
- jest.spyOn(store, 'subscribe');
- jest.spyOn(store, 'unsubscribe');
- const ConnectedChild = connect(Object)(Child);
-
- render(
-
-
- ,
- document.body
- );
-
- expect(store.subscribe).toBeCalledWith(expect.any(Function));
- expect(Child).toHaveBeenCalledWith(
- { store, children: NO_CHILDREN },
- expect.anything()
- );
-
- Child.mockClear();
-
- store.setState({ a: 'b' });
- await sleep(1);
- expect(Child).toHaveBeenCalledWith(
- { a: 'b', store, children: NO_CHILDREN },
- expect.anything()
- );
-
- render(null, document.body);
- expect(store.unsubscribe).toBeCalled();
-
- Child.mockClear();
-
- store.setState({ c: 'd' });
- await sleep(1);
- expect(Child).not.toHaveBeenCalled();
- });
-
- it('should run mapStateToProps and update when outer props change', async () => {
- let state = {};
- const store = { subscribe: jest.fn(), unsubscribe: () => {}, getState: () => state };
- const Child = jest.fn().mockName('').mockReturnValue(42);
- let mappings = 0;
-
- // Jest mock return values are broken :(
- const mapStateToProps = jest.fn((state, props) => ({
- mappings: ++mappings,
- ...props
- }));
-
- const ConnectedChild = connect(mapStateToProps)(Child);
- render(
-
-
- ,
- document.body
- );
-
- expect(mapStateToProps).toHaveBeenCalledTimes(1);
- expect(mapStateToProps).toHaveBeenCalledWith({}, { children: NO_CHILDREN });
- // first render calls mapStateToProps
- expect(Child).toHaveBeenCalledWith(
- { mappings: 1, store, children: NO_CHILDREN },
- expect.anything()
- );
-
- mapStateToProps.mockClear();
- Child.mockClear();
-
- render(
-
-
- ,
- document.body
- );
-
- expect(mapStateToProps).toHaveBeenCalledTimes(1);
- expect(mapStateToProps).toHaveBeenCalledWith({ }, { a: 'b', children: NO_CHILDREN });
- // outer props were changed
- expect(Child).toHaveBeenCalledWith(
- { mappings: 2, a: 'b', store, children: NO_CHILDREN },
- expect.anything()
- );
-
- mapStateToProps.mockClear();
- Child.mockClear();
-
- render(
-
-
- ,
- document.body
- );
-
- expect(mapStateToProps).toHaveBeenCalledTimes(1);
- expect(mapStateToProps).toHaveBeenCalledWith({ }, { a: 'b', children: NO_CHILDREN });
-
- // re-rendered, but outer props were not changed
- expect(Child).toHaveBeenCalledWith(
- { mappings: 3, a: 'b', store, children: NO_CHILDREN },
- expect.anything()
- );
- });
- });
-});
diff --git a/test/react/.babelrc b/test/react/.babelrc
deleted file mode 100644
index aebf8ad..0000000
--- a/test/react/.babelrc
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "presets": [
- [
- "env",
- {
- "targets": {
- "node": true
- }
- }
- ]
- ],
- "plugins": [["transform-react-jsx"]]
-}
diff --git a/test/react/builds.test.js b/test/react/builds.test.js
deleted file mode 100644
index 099bd68..0000000
--- a/test/react/builds.test.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import 'raf/polyfill';
-import React from 'react';
-
-import { mount, configure } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-configure({ adapter: new Adapter() });
-
-import createStore from '../..';
-import react from '../../react';
-
-describe('build: default', () => {
- describe('unistore', () => {
- it('should export only a single function as default', () => {
- expect(createStore).toBeInstanceOf(Function);
- });
- });
-
- describe('unistore/react', () => {
- it('should export connect', () => {
- expect(react).toHaveProperty('connect', expect.any(Function));
- });
- it('should export Provider', () => {
- expect(react).toHaveProperty('Provider', expect.any(Function));
- });
- it('should no export anything else', () => {
- expect(react).toEqual({
- connect: react.connect,
- Provider: react.Provider
- });
- });
- });
-
- describe('smoke test (react)', () => {
- it('should render', done => {
- const { Provider, connect } = react;
- const actions = ({ getState, setState }) => ({
- incrementTwice(state) {
- setState({ count: state.count + 1 });
- return new Promise(r =>
- setTimeout(() => {
- r({ count: getState().count + 1 });
- }, 20)
- );
- }
- });
- const App = connect('count', actions)(({ count, incrementTwice }) => (
-
- count: {count}
-
- ));
- const store = createStore({ count: 0 });
- const provider = (
-
-
-
- );
- const mountedProvider = mount(provider);
- expect(store.getState()).toEqual({ count: 0 });
- const button = mountedProvider.find('#some_button').simulate('click');
- expect(store.getState()).toEqual({ count: 1 });
- setTimeout(() => {
- expect(store.getState()).toEqual({ count: 2 });
- expect(button.text()).toBe('count: 2');
- done();
- }, 30);
- });
- });
-});
diff --git a/test/react/react.test.js b/test/react/react.test.js
deleted file mode 100644
index 00ca647..0000000
--- a/test/react/react.test.js
+++ /dev/null
@@ -1,187 +0,0 @@
-import 'raf/polyfill';
-import React, { Component } from 'react';
-import TestUtils from 'react-dom/test-utils';
-import PropTypes from 'prop-types';
-
-import createStore from '../../src';
-import { Provider, connect } from '../../src/integrations/react';
-
-import { mount, configure } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-configure({ adapter: new Adapter() });
-
-const sleep = ms => new Promise(r => setTimeout(r, ms));
-
-describe('integrations/react', () => {
- const createChild = (storeKey = 'store') => {
- class Child extends Component {
- render() {
- return
;
- }
- }
-
- Child.contextTypes = {
- [storeKey]: PropTypes.object.isRequired
- };
-
- return Child;
- };
- const Child = createChild();
-
- it('should provide props into context', () => {
- const store = createStore(() => ({}));
-
- const spy = jest.spyOn(console, 'error');
- const tree = TestUtils.renderIntoDocument(
-
-
-
- );
- expect(spy).not.toHaveBeenCalled();
-
- const child = TestUtils.findRenderedComponentWithType(tree, Child);
- expect(child.context.store).toBe(store);
- });
-
- it('should pass mapped state as props', () => {
- let state = { a: 'b' };
- const store = { subscribe: jest.fn(), getState: () => state };
- const ConnectedChild = connect(Object)(Child);
-
- const mountedProvider = mount(
-
-
-
- );
-
- const child = mountedProvider.find(Child).first();
- expect(child.props()).toEqual({
- a: 'b',
- store
- });
- expect(store.subscribe).toBeCalled();
- });
-
- it('should transform string selector', () => {
- let state = { a: 'b', b: 'c', c: 'd' };
- const store = { subscribe: jest.fn(), getState: () => state };
- const ConnectedChild = connect('a, b')(Child);
- const mountedProvider = mount(
-
-
-
- );
-
- const child = mountedProvider.find(Child).first();
- expect(child.props()).toEqual({
- a: 'b',
- b: 'c',
- store
- });
- expect(store.subscribe).toBeCalled();
- });
-
- it('should subscribe to store', async () => {
- const store = createStore();
- jest.spyOn(store, 'subscribe');
- jest.spyOn(store, 'unsubscribe');
-
- const ConnectedChild = connect(Object)(Child);
-
- expect(store.subscribe).not.toHaveBeenCalled();
- const mountedProvider = mount(
-
-
-
- );
-
- expect(store.subscribe).toBeCalledWith(expect.any(Function));
-
- let child = mountedProvider
- .find('Child')
- .first()
- .instance();
- expect(child.props).toEqual({ store });
-
- store.setState({ a: 'b' });
- await sleep(1);
-
- child = mountedProvider
- .find('Child')
- .first()
- .instance();
- expect(child.props).toEqual({ a: 'b', store });
-
- expect(store.unsubscribe).not.toHaveBeenCalled();
- mountedProvider.unmount();
- expect(store.unsubscribe).toBeCalled();
- });
-
- it('should run mapStateToProps and update when outer props change', async () => {
- let state = {};
- const store = { subscribe: jest.fn(), getState: () => state };
- const Child = jest.fn(() => null).mockName('');
- let mappings = 0;
-
- // Jest mock return values are broken :(
- const mapStateToProps = jest.fn((state, props) => ({ mappings: ++mappings, ...props }));
-
- let root;
- class Outer extends Component {
- constructor() {
- super();
- this.state = {};
- root = this;
- root.setProps = props => this.setState({ props });
- }
- render() {
- root = this;
- return (
-
-
-
- );
- }
- }
-
- const ConnectedChild = connect(mapStateToProps)(Child);
- const mountedProvider = mount( );
-
- expect(mapStateToProps).toHaveBeenCalledTimes(1);
- expect(mapStateToProps).toHaveBeenCalledWith({}, { });
- // first render calls mapStateToProps
- expect(Child).toHaveBeenCalledWith(
- { mappings: 1, store },
- expect.anything()
- );
-
- mapStateToProps.mockClear();
- Child.mockClear();
-
- // root.setState({ a: 'b' });
- mountedProvider.setProps({ a: 'b' });
-
- // await sleep(100);
-
- expect(mapStateToProps).toHaveBeenCalledTimes(1);
- expect(mapStateToProps).toHaveBeenCalledWith({}, { a: 'b' });
- // outer props were changed
- expect(Child).toHaveBeenCalledWith(
- { mappings: 2, a: 'b', store },
- expect.anything()
- );
-
- mapStateToProps.mockClear();
- Child.mockClear();
-
- mountedProvider.setProps({ });
-
- await sleep(1);
-
- // re-rendered, but outer props were not changed
- expect(Child).toHaveBeenCalledWith(
- { mappings: 3, a: 'b', store },
- expect.anything()
- );
- });
-});
diff --git a/test/unistore.test.js b/test/unistore.test.js
deleted file mode 100644
index 7bc3a40..0000000
--- a/test/unistore.test.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import createStore from '../src';
-
-describe('createStore()', () => {
- it('should be instantiable', () => {
- let store = createStore();
- expect(store).toMatchObject({
- setState: expect.any(Function),
- getState: expect.any(Function),
- subscribe: expect.any(Function),
- unsubscribe: expect.any(Function)
- });
- });
-
- it('should update state in-place', () => {
- let store = createStore();
- expect(store.getState()).toMatchObject({});
- store.setState({ a: 'b' });
- expect(store.getState()).toMatchObject({ a: 'b' });
- store.setState({ c: 'd' });
- expect(store.getState()).toMatchObject({ a: 'b', c: 'd' });
- store.setState({ a: 'x' });
- expect(store.getState()).toMatchObject({ a: 'x', c: 'd' });
- store.setState({ c: null });
- expect(store.getState()).toMatchObject({ a: 'x', c: null });
- store.setState({ c: undefined });
- expect(store.getState()).toMatchObject({ a: 'x', c: undefined });
- });
-
- it('should invoke subscriptions', () => {
- let store = createStore();
-
- let sub1 = jest.fn();
- let sub2 = jest.fn();
- let action;
-
- let rval = store.subscribe(sub1);
- expect(rval).toBeInstanceOf(Function);
-
- store.setState({ a: 'b' });
- expect(sub1).toBeCalledWith(store.getState(), action);
-
- store.subscribe(sub2);
- store.setState({ c: 'd' });
-
- expect(sub1).toHaveBeenCalledTimes(2);
- expect(sub1).toHaveBeenLastCalledWith(store.getState(), action);
- expect(sub2).toBeCalledWith(store.getState(), action);
- });
-
- it('should unsubscribe', () => {
- let store = createStore();
-
- let sub1 = jest.fn();
- let sub2 = jest.fn();
- let sub3 = jest.fn();
-
- store.subscribe(sub1);
- store.subscribe(sub2);
- let unsub3 = store.subscribe(sub3);
-
- store.setState({ a: 'b' });
- expect(sub1).toBeCalled();
- expect(sub2).toBeCalled();
- expect(sub3).toBeCalled();
-
- sub1.mockClear();
- sub2.mockClear();
- sub3.mockClear();
-
- store.unsubscribe(sub2);
-
- store.setState({ c: 'd' });
- expect(sub1).toBeCalled();
- expect(sub2).not.toBeCalled();
- expect(sub3).toBeCalled();
-
- sub1.mockClear();
- sub2.mockClear();
- sub3.mockClear();
-
- store.unsubscribe(sub1);
-
- store.setState({ e: 'f' });
- expect(sub1).not.toBeCalled();
- expect(sub2).not.toBeCalled();
- expect(sub3).toBeCalled();
-
- sub3.mockClear();
-
- unsub3();
-
- store.setState({ g: 'h' });
- expect(sub1).not.toBeCalled();
- expect(sub2).not.toBeCalled();
- expect(sub3).not.toBeCalled();
- });
-});
diff --git a/unistore.js b/unistore.js
new file mode 100644
index 0000000..af58e82
--- /dev/null
+++ b/unistore.js
@@ -0,0 +1,115 @@
+import { h, Component } from 'preact';
+
+
+/** Creates a new store, which is a tiny evented state container.
+ * @example
+ * let store = createStore();
+ * store.subscribe( state => console.log(state) );
+ * store.setState({ a: 'b' }); // logs { a: 'b' }
+ * store.setState({ c: 'd' }); // logs { c: 'd' }
+ */
+export function createStore(state={}) {
+ let listeners = [];
+
+ return {
+ setState(update) {
+ state = { ...state, ...update };
+ listeners.forEach( f => { f(state); });
+ },
+ subscribe(f) {
+ listeners.push(f);
+ },
+ unsubscribe(f) {
+ let i = listeners.indexOf(f);
+ listeners.splice(i, !!~i);
+ },
+ getState() {
+ return state;
+ }
+ };
+}
+
+
+/** Provides its props into the tree as context.
+ * @example
+ * let store = createStore();
+ *
+ */
+export class Provider extends Component {
+ getChildContext() {
+ let context = { ...this.props };
+ delete context.children;
+ return context;
+ }
+ render({ children }) {
+ return children[0];
+ }
+}
+
+
+/** Wire a component up to the store. Passes state as props, re-renders on change.
+ * @param {Function|Array|String} mapStateToProps A function (or any `select()` argument) mapping of store state to prop values.
+ * @example
+ * const Foo = connect('foo,bar')( ({ foo, bar }) =>
)
+ * @example
+ * @connect( state => ({ foo: state.foo, bar: state.bar }) )
+ * export class Foo { render({ foo, bar }) { } }
+ */
+export function connect(mapToProps, actions) {
+ if (typeof mapToProps!=='function') mapToProps = select(mapToProps);
+ return Child => (
+ class Wrapper extends Component {
+ constructor(props, { store }) {
+ super();
+ this.state = mapToProps(store ? store.getState() : {}, props);
+ this.actions = actions ? actions(store) : { store };
+ this.update = () => {
+ let mapped = mapToProps(store ? store.getState() : {}, this.props);
+ if (!shallowEqual(mapped, this.state)) {
+ this.setState(mapped);
+ }
+ };
+ }
+ componentDidMount() {
+ this.context.store.subscribe(this.update);
+ }
+ componentWillUnmount() {
+ this.context.store.unsubscribe(this.update);
+ }
+ render(props, state) {
+ return ;
+ }
+ }
+ );
+}
+
+
+/** select('foo,bar') creates a function of the form: ({ foo, bar }) => ({ foo, bar }) */
+export function select(properties) {
+ if (typeof properties==='string') properties = properties.split(',');
+ return state => {
+ let selected = {};
+ for (let i=0; i new Promise( r => setTimeout(r, ms) );
+
+describe('createStore()', () => {
+ it('should be instantiable', () => {
+ let store = createStore();
+ expect(store).toMatchObject({
+ setState: expect.any(Function),
+ getState: expect.any(Function),
+ subscribe: expect.any(Function),
+ unsubscribe: expect.any(Function)
+ });
+ });
+
+ it('should update state in-place', () => {
+ let store = createStore();
+ expect(store.getState()).toMatchObject({});
+ store.setState({ a: 'b' });
+ expect(store.getState()).toMatchObject({ a: 'b' });
+ store.setState({ c: 'd' });
+ expect(store.getState()).toMatchObject({ a: 'b', c: 'd' });
+ store.setState({ a: 'x' });
+ expect(store.getState()).toMatchObject({ a: 'x', c: 'd' });
+ store.setState({ c: null });
+ expect(store.getState()).toMatchObject({ a: 'x', c: null });
+ store.setState({ c: undefined });
+ expect(store.getState()).toMatchObject({ a: 'x', c: undefined });
+ });
+
+ it('should invoke subscriptions', () => {
+ let store = createStore();
+
+ let sub1 = jest.fn();
+ let sub2 = jest.fn();
+
+ let rval = store.subscribe(sub1);
+ expect(rval).toBe(undefined);
+
+ store.setState({ a: 'b' });
+ expect(sub1).toBeCalledWith(store.getState());
+
+ store.subscribe(sub2);
+ store.setState({ c: 'd' });
+
+ expect(sub1).toHaveBeenCalledTimes(2);
+ expect(sub1).toHaveBeenLastCalledWith(store.getState());
+ expect(sub2).toBeCalledWith(store.getState());
+ });
+
+ it('should unsubscribe', () => {
+ let store = createStore();
+
+ let sub1 = jest.fn();
+ let sub2 = jest.fn();
+
+ store.subscribe(sub1);
+ store.subscribe(sub2);
+
+ store.setState({ a: 'b' });
+ expect(sub1).toBeCalled();
+ expect(sub2).toBeCalled();
+
+ sub1.mockReset();
+ sub2.mockReset();
+
+ store.unsubscribe(sub2);
+
+ store.setState({ c: 'd' });
+ expect(sub1).toBeCalled();
+ expect(sub2).not.toBeCalled();
+
+ sub1.mockReset();
+ sub2.mockReset();
+
+ store.unsubscribe(sub1);
+
+ store.setState({ e: 'f' });
+ expect(sub1).not.toBeCalled();
+ expect(sub2).not.toBeCalled();
+ });
+});
+
+describe('', () => {
+ it('should provide props into context', () => {
+ const Child = jest.fn();
+ let obj = { name: 'obj' };
+ render( , document.body);
+ expect(Child).toHaveBeenCalledWith(expect.anything(), { a: 'a', obj });
+ });
+});
+
+describe('connect()', () => {
+ it('should pass mapped state as props', () => {
+ let state = { a: 'b' };
+ const store = { subscribe: jest.fn(), getState: () => state };
+ const Child = jest.fn();
+ const ConnectedChild = connect(Object)(Child);
+ render( , document.body);
+ expect(Child).toHaveBeenCalledWith({ a: 'b', store, children: expect.anything() }, expect.anything());
+ expect(store.subscribe).toBeCalled();
+ });
+
+ it('should subscribe to store', async () => {
+ const store = createStore();
+ const Child = jest.fn();
+ jest.spyOn(store, 'subscribe');
+ jest.spyOn(store, 'unsubscribe');
+ const ConnectedChild = connect(Object)(Child);
+
+ let root = render( , document.body);
+
+ expect(store.subscribe).toBeCalledWith(expect.any(Function));
+ expect(Child).toHaveBeenCalledWith({ store, children: expect.anything() }, expect.anything());
+
+ Child.mockReset();
+
+ store.setState({ a: 'b' });
+ await sleep(1);
+ expect(Child).toHaveBeenCalledWith({ a: 'b', store, children: expect.anything() }, expect.anything());
+
+ render(null, document.body, root);
+ expect(store.unsubscribe).toBeCalled();
+
+ Child.mockReset();
+
+ store.setState({ c: 'd' });
+ await sleep(1);
+ expect(Child).not.toHaveBeenCalled();
+ });
+});
\ No newline at end of file