├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── examples ├── redux-todos │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ ├── index.js │ │ └── index.spec.js │ │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ └── VisibleTodoList.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ ├── todos.js │ │ ├── todos.spec.js │ │ └── visibilityFilter.js └── simple │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ └── src │ ├── Timer.js │ └── index.js ├── index.d.ts ├── package.json ├── src ├── component.js ├── customCreateElement.js ├── customRxOperators.js ├── forceArray.js ├── forceArray.test.js ├── getNodeSelectors.js ├── index.js ├── registerListeners.js ├── shallowClone.js ├── shallowClone.test.js └── updateNodeStreams.js └── tests └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | ["transform-react-jsx"], 5 | "transform-object-rest-spread" 6 | ] 7 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "standard-react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated code 2 | /index.js 3 | /lib 4 | /dist 5 | /coverage 6 | 7 | node_modules 8 | .DS_Store 9 | npm-debug.log 10 | _book 11 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recyclejs/recycle/948383b5efeee13db24cf8a4101b5bdb6ef7b19b/.npmignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Join the chat at https://gitter.im/recyclejs](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/recyclejs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | [![npm version](https://img.shields.io/npm/v/recycle.svg?style=flat-square)](https://www.npmjs.com/package/recycle) 3 | [![npm downloads](https://img.shields.io/npm/dm/recycle.svg?style=flat-square)](https://www.npmjs.com/package/recycle) 4 | 5 | # DEPRECATED 6 | Please note that this library hasn't been updated for more than two years. It's very rarely used and I consider it deprecated. 7 | 8 | # Recycle 9 | Convert functional/reactive object description into React component. 10 | 11 | You don't need another UI framework if you want to use [RxJS](https://github.com/ReactiveX/rxjs). 12 | 13 | ## Installation 14 | ```bash 15 | npm install --save recycle 16 | ``` 17 | 18 | ## Example 19 | [**Webpackbin example**](https://www.webpackbin.com/bins/-KiHSPOMjmY9tz4qYnbv) 20 | 21 | ```javascript 22 | const Timer = recycle({ 23 | initialState: { 24 | secondsElapsed: 0, 25 | counter: 0 26 | }, 27 | 28 | update (sources) { 29 | return [ 30 | sources.select('button') 31 | .addListener('onClick') 32 | .reducer(state => { 33 | ...state, 34 | counter: state.counter + 1 35 | }), 36 | 37 | Rx.Observable.interval(1000) 38 | .reducer(state => { 39 | ...state, 40 | secondsElapsed: state.secondsElapsed + 1 41 | }) 42 | ] 43 | }, 44 | 45 | view (props, state) { 46 | return ( 47 |
48 |
Seconds Elapsed: {state.secondsElapsed}
49 |
Times Clicked: {state.counter}
50 | 51 |
52 | ) 53 | } 54 | }) 55 | ``` 56 | 57 | You can also listen on child component events and define custom event handlers. 58 | Just make sure you specify what should be returned: 59 | 60 | ```javascript 61 | import CustomButton from './CustomButton' 62 | 63 | const Timer = recycle({ 64 | initialState: { 65 | counter: 0 66 | }, 67 | 68 | update (sources) { 69 | return [ 70 | sources.select(CustomButton) 71 | .addListener('customOnClick') 72 | .reducer((state, returnedValue) => { 73 | counter: state.counter + returnedValue 74 | }) 75 | ] 76 | }, 77 | 78 | view (props, state) { 79 | return ( 80 |
81 |
Times Clicked: {state.counter}
82 | e.something}>Click Me 83 |
84 | ) 85 | } 86 | }) 87 | ``` 88 | 89 | ## Replacing Redux Connect 90 | If you are using Redux, 91 | Recycle component can also be used as a container (an alternative to Redux `connect`). 92 | 93 | The advantage of this approach is that you have full control over component rerendering (components will not be "forceUpdated" magically). 94 | 95 | Also, you can listen to a specific part of the state and update your component only if that property is changed. 96 | 97 | ```javascript 98 | export default recycle({ 99 | dispatch (sources) { 100 | return [ 101 | sources.select('div') 102 | .addListener('onClick') 103 | .mapTo({ type: 'REDUX_ACTION_TYPE', text: 'hello from recycle' }) 104 | ] 105 | }, 106 | 107 | update (sources) { 108 | return [ 109 | sources.store 110 | .reducer(function (state, store) { 111 | return store 112 | }) 113 | 114 | /** 115 | * Example of a subscription on a specific store property 116 | * with distinctUntilChanged() component will be updated only when that property is changed 117 | * 118 | * sources.store 119 | * .map(s => s.specificProperty) 120 | * .distinctUntilChanged() 121 | * .reducer(function (state, specificProperty) { 122 | * state.something = specificProperty 123 | * return state 124 | * }) 125 | */ 126 | ] 127 | }, 128 | 129 | view (props, state) { 130 | return
Number of todos: {store.todos.length}
131 | } 132 | }) 133 | ``` 134 | 135 | ## Effects 136 | If you don't need to update a component local state or dispatch Redux action, 137 | but you still need to react to some kind of async operation, you can use `effects`. 138 | 139 | Recycle will subscribe to this stream but it will not use it. 140 | It is intended for making side effects (like calling callback functions passed from a parent component) 141 | 142 | ```javascript 143 | const Timer = recycle({ 144 | 145 | effects (sources) { 146 | return [ 147 | sources.select('input') 148 | .addListener('onKeyPress') 149 | .withLatestFrom(sources.props) 150 | .map(([e, props]) => { 151 | props.callParentFunction(e.target.value) 152 | }) 153 | ] 154 | }, 155 | 156 | view (props) { 157 | return ( 158 | 159 | ) 160 | } 161 | }) 162 | ``` 163 | 164 | ## API 165 | Component description object accepts following properties: 166 | 167 | ```javascript 168 | { 169 | propTypes: { name: PropTypes.string }, 170 | displayName: 'ComponentName', 171 | initialState: {}, 172 | dispatch: function(sources) { return Observable }, 173 | update: function(sources) { return Observable }, 174 | effects: function(sources) { return Observable }, 175 | view: function(props, state) { return JSX } 176 | } 177 | ``` 178 | 179 | In `update`, `dispatch` and `effects` functions, you can use the following sources: 180 | 181 | ```javascript 182 | /** 183 | * sources.select 184 | * 185 | * select node by tag name or child component 186 | */ 187 | sources.select('tag') 188 | .addListener('event') 189 | 190 | sources.select(ChildComponent) 191 | .addListener('event') 192 | 193 | /** 194 | * sources.selectClass 195 | * 196 | * select node by class name 197 | */ 198 | sources.selectClass('classname') 199 | .addListener('event') 200 | 201 | /** 202 | * sources.selectId 203 | * 204 | * select node by its id 205 | */ 206 | sources.selectId('node-id') 207 | .addListener('event') 208 | 209 | /** 210 | * sources.store 211 | * 212 | * If you are using redux (component is inside Provider) 213 | * sources.store will emit its state changes 214 | */ 215 | sources.store 216 | .reducer(...) 217 | 218 | /** 219 | * sources.state 220 | * 221 | * Stream of current local component state 222 | */ 223 | sources.select('input') 224 | .addListener('onKeyPress') 225 | .filter(e => e.key === 'Enter') 226 | .withLatestFrom(sources.state) 227 | .map(([e, state]) => state.someStateValue) 228 | .map(someStateValue => using(someStateValue)) 229 | 230 | /** 231 | * sources.props 232 | * 233 | * Stream of current local component props 234 | */ 235 | sources.select('input') 236 | .addListener('onKeyPress') 237 | .filter(e => e.key === 'Enter') 238 | .withLatestFrom(sources.props) 239 | .map(([e, props]) => props.somePropsValue) 240 | .map(somePropsValue => using(somePropsValue)) 241 | 242 | /** 243 | * sources.lifecycle 244 | * 245 | * Stream of component lifecycle events 246 | */ 247 | sources.lifecycle 248 | .filter(e => e === 'componentDidMount') 249 | .do(something) 250 | ``` 251 | 252 | ## FAQ 253 | 254 | ### Why would I use it? 255 | - Greater separation of concerns between component presentation and component logic 256 | - You don't need classes so each part of a component can be defined and tested separately. 257 | - Component description is more consistent. 258 | There is no custom `handleClick` events or `this.setState` statements that you need to worry about. 259 | - The State is calculated the same way as for redux store: `state = reducer(state, action)`. 260 | - Redux container looks like a normal component and it's more clear what it does. 261 | - Easy to use in an existing React application (choose components which you wish to convert). 262 | 263 | ### Why would I NOT use it? 264 | - Observables are not your thing. 265 | - You need more control over component lifecycle (like `shouldComponentUpdate`) 266 | 267 | ### What is this? jQuery? 268 | No. 269 | 270 | Although it resembles [query selectors](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector), Recycle uses React’s inline event handlers and doesn’t rely on the DOM. Since selection is isolated per component, no child nodes can ever be accessed. 271 | 272 | ### Can I use CSS selectors? 273 | No. 274 | 275 | Since Recycle doesn't query over your nodes, selectors like `div .class` will not work. 276 | 277 | ### How does it then find selected nodes? 278 | It works by monkeypatching `React.createElement`. 279 | Before a component is rendered, for each element, 280 | if a select query is matched, recycle sets inline event listener. 281 | 282 | Each time event handler dispatches an event, 283 | it calls `selectedNode.rxSubject.next(e)` 284 | 285 | ### Can I use it with React Native? 286 | Yes. 287 | 288 | Recycle creates classical React component which can be safely used in React Native. 289 | -------------------------------------------------------------------------------- /examples/redux-todos/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /examples/redux-todos/README.md: -------------------------------------------------------------------------------- 1 | # Redux Todos Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | 37 | -------------------------------------------------------------------------------- /examples/redux-todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.4.1", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^0.9.3" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "eject": "react-scripts eject", 14 | "test": "react-scripts test" 15 | }, 16 | "dependencies": { 17 | "prop-types": "^15.6.0", 18 | "react": "^16.1.1", 19 | "react-dom": "^16.1.1", 20 | "react-redux": "^5.0.6", 21 | "recycle": "^3.0.0", 22 | "redux": "^3.7.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/redux-todos/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Todos Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/redux-todos/src/actions/index.js: -------------------------------------------------------------------------------- 1 | let nextTodoId = 0 2 | export const addTodo = (text) => ({ 3 | type: 'ADD_TODO', 4 | id: nextTodoId++, 5 | text 6 | }) 7 | 8 | export const setVisibilityFilter = (filter) => ({ 9 | type: 'SET_VISIBILITY_FILTER', 10 | filter 11 | }) 12 | 13 | export const toggleTodo = (id) => ({ 14 | type: 'TOGGLE_TODO', 15 | id 16 | }) 17 | -------------------------------------------------------------------------------- /examples/redux-todos/src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as actions from './index' 2 | 3 | describe('todo actions', () => { 4 | it('addTodo should create ADD_TODO action', () => { 5 | expect(actions.addTodo('Use Redux')).toEqual({ 6 | type: 'ADD_TODO', 7 | id: 0, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('setVisibilityFilter should create SET_VISIBILITY_FILTER action', () => { 13 | expect(actions.setVisibilityFilter('active')).toEqual({ 14 | type: 'SET_VISIBILITY_FILTER', 15 | filter: 'active' 16 | }) 17 | }) 18 | 19 | it('toggleTodo should create TOGGLE_TODO action', () => { 20 | expect(actions.toggleTodo(1)).toEqual({ 21 | type: 'TOGGLE_TODO', 22 | id: 1 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /examples/redux-todos/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Footer from './Footer' 3 | import AddTodo from '../containers/AddTodo' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | 6 | const App = () => ( 7 |
8 | 9 | 10 |
12 | ) 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /examples/redux-todos/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FilterLink from '../containers/FilterLink' 3 | 4 | const Footer = () => ( 5 |

6 | Show: 7 | {" "} 8 | 9 | All 10 | 11 | {", "} 12 | 13 | Active 14 | 15 | {", "} 16 | 17 | Completed 18 | 19 |

20 | ) 21 | 22 | export default Footer 23 | -------------------------------------------------------------------------------- /examples/redux-todos/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Link = ({ active, children, onClick }) => { 5 | if (active) { 6 | return {children} 7 | } 8 | 9 | return ( 10 | { 12 | e.preventDefault() 13 | onClick() 14 | }} 15 | > 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | Link.propTypes = { 22 | active: PropTypes.bool.isRequired, 23 | children: PropTypes.node.isRequired, 24 | onClick: PropTypes.func.isRequired 25 | } 26 | 27 | export default Link 28 | -------------------------------------------------------------------------------- /examples/redux-todos/src/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Todo = ({ onClick, completed, text }) => ( 5 |
  • 11 | {text} 12 |
  • 13 | ) 14 | 15 | Todo.propTypes = { 16 | onClick: PropTypes.func.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired 19 | } 20 | 21 | export default Todo 22 | -------------------------------------------------------------------------------- /examples/redux-todos/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Todo from './Todo' 4 | 5 | const TodoList = ({ todos, onTodoClick }) => ( 6 | 15 | ) 16 | 17 | TodoList.propTypes = { 18 | todos: PropTypes.arrayOf(PropTypes.shape({ 19 | id: PropTypes.number.isRequired, 20 | completed: PropTypes.bool.isRequired, 21 | text: PropTypes.string.isRequired 22 | }).isRequired).isRequired, 23 | onTodoClick: PropTypes.func.isRequired 24 | } 25 | 26 | export default TodoList 27 | -------------------------------------------------------------------------------- /examples/redux-todos/src/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import recycle from 'recycle' 3 | import { addTodo } from '../actions' 4 | 5 | const AddTodo = recycle({ 6 | initialState: { 7 | inputVal: '' 8 | }, 9 | 10 | dispatch (sources) { 11 | return [ 12 | sources.select('form') 13 | .addListener('onSubmit') 14 | .withLatestFrom(sources.state) 15 | .map(([e, state]) => addTodo(state.inputVal)) 16 | ] 17 | }, 18 | 19 | update (sources) { 20 | return [ 21 | sources.select('input') 22 | .addListener('onChange') 23 | .reducer(function (state, e) { 24 | return { 25 | ...state, 26 | inputVal: e.target.value 27 | } 28 | }), 29 | 30 | sources.select('form') 31 | .addListener('onSubmit') 32 | .reducer(function (state, e) { 33 | e.preventDefault() 34 | return { 35 | ...state, 36 | inputVal: '' 37 | } 38 | }) 39 | ] 40 | }, 41 | 42 | view (props, state) { 43 | return ( 44 |
    45 |
    46 | 47 | 50 |
    51 |
    52 | ) 53 | } 54 | }) 55 | 56 | export default AddTodo 57 | -------------------------------------------------------------------------------- /examples/redux-todos/src/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import recycle from 'recycle' 3 | import { setVisibilityFilter } from '../actions' 4 | 5 | const FilterLink = recycle({ 6 | dispatch (sources) { 7 | return [ 8 | sources.select('a') 9 | .addListener('onClick') 10 | .map(e => e.preventDefault()) 11 | .withLatestFrom(sources.props) 12 | .map(([e, props]) => setVisibilityFilter(props.filter)) 13 | ] 14 | }, 15 | 16 | update (sources) { 17 | return [ 18 | sources.store 19 | // maping store to the component state 20 | .reducer((state, store) => store) 21 | ] 22 | }, 23 | 24 | view (props, state) { 25 | if (props.filter === state.visibilityFilter) { 26 | return {props.children} 27 | } 28 | 29 | return {props.children} 30 | } 31 | }) 32 | 33 | export default FilterLink 34 | -------------------------------------------------------------------------------- /examples/redux-todos/src/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import recycle from 'recycle' 3 | import { toggleTodo } from '../actions' 4 | import TodoList from '../components/TodoList' 5 | 6 | const getVisibleTodos = (todos, filter) => { 7 | switch (filter) { 8 | case 'SHOW_ALL': 9 | return todos 10 | case 'SHOW_COMPLETED': 11 | return todos.filter(t => t.completed) 12 | case 'SHOW_ACTIVE': 13 | return todos.filter(t => !t.completed) 14 | default: 15 | throw new Error('Unknown filter: ' + filter) 16 | } 17 | } 18 | 19 | const VisibleTodoList = recycle({ 20 | dispatch (sources) { 21 | return sources.select(TodoList) 22 | .addListener('onTodoClick') 23 | .map(toggleTodo) 24 | }, 25 | 26 | update (sources) { 27 | return sources.store 28 | // maping store to the component state 29 | .reducer((state, store) => store) 30 | }, 31 | 32 | view (props, state) { 33 | return 34 | } 35 | }) 36 | 37 | export default VisibleTodoList 38 | -------------------------------------------------------------------------------- /examples/redux-todos/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import App from './components/App' 6 | import reducer from './reducers' 7 | import 'rxjs/add/operator/withLatestFrom' 8 | 9 | const store = createStore(reducer) 10 | 11 | render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ) 17 | -------------------------------------------------------------------------------- /examples/redux-todos/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | import visibilityFilter from './visibilityFilter' 4 | 5 | const todoApp = combineReducers({ 6 | todos, 7 | visibilityFilter 8 | }) 9 | 10 | export default todoApp 11 | -------------------------------------------------------------------------------- /examples/redux-todos/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | const todo = (state, action) => { 2 | switch (action.type) { 3 | case 'ADD_TODO': 4 | return { 5 | id: action.id, 6 | text: action.text, 7 | completed: false 8 | } 9 | case 'TOGGLE_TODO': 10 | if (state.id !== action.id) { 11 | return state 12 | } 13 | 14 | return { 15 | ...state, 16 | completed: !state.completed 17 | } 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | const todos = (state = [], action) => { 24 | switch (action.type) { 25 | case 'ADD_TODO': 26 | return [ 27 | ...state, 28 | todo(undefined, action) 29 | ] 30 | case 'TOGGLE_TODO': 31 | return state.map(t => 32 | todo(t, action) 33 | ) 34 | default: 35 | return state 36 | } 37 | } 38 | 39 | export default todos 40 | -------------------------------------------------------------------------------- /examples/redux-todos/src/reducers/todos.spec.js: -------------------------------------------------------------------------------- 1 | import todos from './todos' 2 | 3 | describe('todos reducer', () => { 4 | it('should handle initial state', () => { 5 | expect( 6 | todos(undefined, {}) 7 | ).toEqual([]) 8 | }) 9 | 10 | it('should handle ADD_TODO', () => { 11 | expect( 12 | todos([], { 13 | type: 'ADD_TODO', 14 | text: 'Run the tests', 15 | id: 0 16 | }) 17 | ).toEqual([ 18 | { 19 | text: 'Run the tests', 20 | completed: false, 21 | id: 0 22 | } 23 | ]) 24 | 25 | expect( 26 | todos([ 27 | { 28 | text: 'Run the tests', 29 | completed: false, 30 | id: 0 31 | } 32 | ], { 33 | type: 'ADD_TODO', 34 | text: 'Use Redux', 35 | id: 1 36 | }) 37 | ).toEqual([ 38 | { 39 | text: 'Run the tests', 40 | completed: false, 41 | id: 0 42 | }, { 43 | text: 'Use Redux', 44 | completed: false, 45 | id: 1 46 | } 47 | ]) 48 | 49 | expect( 50 | todos([ 51 | { 52 | text: 'Run the tests', 53 | completed: false, 54 | id: 0 55 | }, { 56 | text: 'Use Redux', 57 | completed: false, 58 | id: 1 59 | } 60 | ], { 61 | type: 'ADD_TODO', 62 | text: 'Fix the tests', 63 | id: 2 64 | }) 65 | ).toEqual([ 66 | { 67 | text: 'Run the tests', 68 | completed: false, 69 | id: 0 70 | }, { 71 | text: 'Use Redux', 72 | completed: false, 73 | id: 1 74 | }, { 75 | text: 'Fix the tests', 76 | completed: false, 77 | id: 2 78 | } 79 | ]) 80 | }) 81 | 82 | it('should handle TOGGLE_TODO', () => { 83 | expect( 84 | todos([ 85 | { 86 | text: 'Run the tests', 87 | completed: false, 88 | id: 1 89 | }, { 90 | text: 'Use Redux', 91 | completed: false, 92 | id: 0 93 | } 94 | ], { 95 | type: 'TOGGLE_TODO', 96 | id: 1 97 | }) 98 | ).toEqual([ 99 | { 100 | text: 'Run the tests', 101 | completed: true, 102 | id: 1 103 | }, { 104 | text: 'Use Redux', 105 | completed: false, 106 | id: 0 107 | } 108 | ]) 109 | }) 110 | 111 | }) 112 | -------------------------------------------------------------------------------- /examples/redux-todos/src/reducers/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | const visibilityFilter = (state = 'SHOW_ALL', action) => { 2 | switch (action.type) { 3 | case 'SET_VISIBILITY_FILTER': 4 | return action.filter 5 | default: 6 | return state 7 | } 8 | } 9 | 10 | export default visibilityFilter 11 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.9.5" 7 | }, 8 | "scripts": { 9 | "start": "react-scripts start", 10 | "build": "react-scripts build", 11 | "test": "react-scripts test --env=jsdom", 12 | "eject": "react-scripts eject" 13 | }, 14 | "dependencies": { 15 | "react": "^16.1.1", 16 | "react-dom": "^16.1.1", 17 | "recycle": "^3.0.0", 18 | "rxjs": "^5.5.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/simple/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recyclejs/recycle/948383b5efeee13db24cf8a4101b5bdb6ef7b19b/examples/simple/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
    20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/simple/src/Timer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import recycle from 'recycle' 3 | import { reducer } from 'recycle/lib/customRxOperators' // optional 4 | import Rx from 'rxjs' 5 | 6 | const Timer = recycle({ 7 | initialState: { 8 | secondsElapsed: 0, 9 | counter: 0 10 | }, 11 | 12 | update (sources) { 13 | return [ 14 | sources.select('button') 15 | .addListener('onClick') 16 | .reducer(function (state) { 17 | return { 18 | ...state, 19 | counter: state.counter + 1 20 | } 21 | }), 22 | 23 | Rx.Observable.interval(1000) 24 | // if you don't want to use custom Rx operator 25 | // you can use "let" 26 | .let(reducer(function (state) { 27 | return { 28 | ...state, 29 | secondsElapsed: state.secondsElapsed + 1 30 | } 31 | })) 32 | ] 33 | }, 34 | 35 | view (props, state) { 36 | return ( 37 |
    38 |
    Seconds Elapsed: {state.secondsElapsed}
    39 |
    Times Clicked: {state.counter}
    40 | 41 |
    42 | ) 43 | } 44 | }) 45 | 46 | export default Timer 47 | -------------------------------------------------------------------------------- /examples/simple/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import Timer from './Timer' 4 | 5 | render(, document.getElementById('root')) 6 | 7 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import React from 'react' 3 | import * as PropTypes from 'prop-types' 4 | import { AnyAction } from 'redux' 5 | 6 | 7 | export declare const recycle: recycle.Recycle 8 | export default recycle 9 | export as namespace recycle 10 | 11 | 12 | type ReducerFn = (state: S, value?: any, store?: R) => S 13 | type ReducerObservableFn = (stream: Observable) => Observable<{ reducer: ReducerFn, event: Event }> 14 | 15 | 16 | type DomEvent = 17 | 'onClick' | 18 | 'onKeyUp' | 19 | 'onKeyDown' | 20 | 'onKeyPress' | 21 | 'onBlur' | 22 | 'onChange' | 23 | 'onClose' | 24 | 'onFocus' | 25 | 'onInput' | 26 | 'onMouseMove' | 27 | 'onMouseDown' | 28 | 'onMouseUp' | 29 | 'onMouseOut' | 30 | 'onMouseOver' | 31 | 'onScroll' | 32 | 'onSubmit' | 33 | 'onSelect' | 34 | 'onTouchMove' | 35 | 'onTouchCancel' | 36 | 'onTouchStart' | 37 | 'onWheel' 38 | 39 | 40 | declare module 'rxjs/Observable' { 41 | interface Observable { 42 | /** 43 | * Acts like a Redux reducer, it receives you local component state (and your Redux state, if you are using Redux), 44 | * and returns a new local state 45 | * 46 | * @template S Your Component local state 47 | * @template R Your Redux state 48 | * @param {(state: S, store: R) => S} reducerFn PS: Param store only appears if you are using Redux (Component is inside Provider) 49 | * @example 50 | * 51 | * sources.select('button') 52 | * .addListener('onClick') 53 | * .reducer((state, eventValue) => { 54 | * ...state, 55 | * counter: state.counter + eventValue 56 | * }) 57 | * 58 | */ 59 | reducer(reducerFn: ReducerFn): ReducerObservableFn 60 | } 61 | } 62 | 63 | 64 | declare namespace recycle { 65 | 66 | interface Listeners { 67 | 68 | /** 69 | * Register a new DOM listener and returns a stream of DOM events 70 | * 71 | * @param event DOM Event to listen to 72 | * @returns {Observable} 73 | * @example 74 | * 75 | * sources.select('button') 76 | * .addListener('onClick') 77 | * .reducer(state => { 78 | * ...state, 79 | * counter: state.counter + 1 80 | * }), 81 | * 82 | */ 83 | addListener: (event: T) => Observable 84 | } 85 | 86 | type ReactLifeCycle = 87 | 'componentWillMount' | 88 | 'componentDidMount' | 89 | 'componentWillReceiveProps' | 90 | 'shouldComponentUpdate' | 91 | 'componentWillUpdate' | 92 | 'componentDidUpdate' | 93 | 'componentWillUnmount' 94 | 95 | interface Sources { 96 | 97 | /** 98 | * This method selects a element by tagName or a ChildComponent and returns a recycle.Listeners 99 | * 100 | * @param param 101 | * @returns {recycle.Listeners} Returns { addListener: (event: DomEvent) => Observable } 102 | * @example 103 | * 104 | * PS: Assuming i have a 105 | * sources.select('button') 106 | * 107 | * or 108 | * 109 | * PS: Assuming i have a as a child element. 110 | * sources.select(ChildComponent) 111 | * 112 | */ 113 | select: (param: string | React.Component | React.StatelessComponent) => recycle.Listeners; 114 | 115 | /** 116 | * This method selects a element by a CSS class and returns a recycle.Listeners 117 | * 118 | * @param className 119 | * @returns {recycle.Listeners} Returns { addListener: (event: DomEvent) => Observable } 120 | * @example 121 | * 122 | * * PS: Assuming i have a as a child element. 123 | * sources.select('cssClass') 124 | * 125 | */ 126 | selectClass: (className: string) => recycle.Listeners; 127 | 128 | /** 129 | * This method selects a element by id and returns a recycle.Listeners 130 | * 131 | * @param id 132 | * @returns {recycle.Listeners} Returns { addListener: (event: DomEvent) => Observable } 133 | * @example 134 | * 135 | * PS: Assuming i have a as a child element. 136 | * sources.select('elementId') 137 | * 138 | */ 139 | selectId: (id: string) => recycle.Listeners 140 | 141 | /** 142 | * If you are using Redux (component is inside Provider) this will return a stream of your redux state 143 | * 144 | * @template R is a interface representing your Redux state 145 | * @returns {Observable} Returns a Observable with your latest Redux state 146 | * @example 147 | * 148 | * PS: Assuming R is { count: number } 149 | * sources.store.map((state: { count: number }) => state.count) 150 | * 151 | */ 152 | store?: Observable 153 | 154 | /** 155 | * Returns a stream of your local component state 156 | * 157 | * @template S is a interface representing your Component state 158 | * @returns {Observable} Returns a Observable with your latest component state 159 | * @example 160 | * 161 | * sources.select('input') 162 | * .addListener('onKeyPress') 163 | * .filter(e => e.key === 'Enter') 164 | * .withLatestFrom(sources.state) 165 | * .map(([e, state]) => state.someStateValue) 166 | * 167 | */ 168 | state: Observable 169 | 170 | /** 171 | * Returns a stream of component lifecycle events 172 | * 173 | * @returns {Observable} 174 | * @example 175 | * 176 | * sources.lifecycle 177 | * .filter(e => e === 'componentDidMount') 178 | * .do(something) 179 | * 180 | */ 181 | lifecycle: Observable 182 | } 183 | 184 | interface Params { 185 | /** 186 | * React props passed by JSX 187 | */ 188 | propTypes?: PropTypes.ValidationMap

    189 | 190 | /** 191 | * Determines the html tag name 192 | */ 193 | displayName?: string 194 | 195 | /** 196 | * Component's local initial state 197 | */ 198 | initialState?: S 199 | 200 | /** 201 | * 202 | * 203 | * @param sources 204 | * @returns {Observable[]} Array of Redux action streams 205 | */ 206 | dispatch?: (sources: recycle.Sources) => Observable[] 207 | 208 | /** 209 | * Acts like a Redux reducer for the component local state 210 | * 211 | * @param sources 212 | * @template S Interface representing the component local state 213 | * @template R Interface representing the Redux state (If using Redux) 214 | * @returns {Observable>[]} 215 | * @example 216 | * 217 | * update: (sources) => { 218 | * return [ 219 | * sources.store 220 | * .reducer(function (state, store) { 221 | * return state 222 | * }) 223 | * ] 224 | * }, 225 | * 226 | */ 227 | update?: (sources: recycle.Sources) => ReducerObservableFn[] 228 | 229 | /** 230 | * If you don't need to update a component local state or dispatch Redux action, but you still need to react 231 | * to some kind of async operation, you can use effects. 232 | * 233 | * @param sources 234 | * @returns Observable[] 235 | * @example 236 | * 237 | * effects: (sources) => { 238 | * return [ 239 | * sources.select('input') 240 | * .addListener('onKeyPress') 241 | * .withLatestFrom(sources.props) 242 | * .map(([e, props]) => { 243 | * props.callParentFunction(e.target.value) 244 | * }) 245 | * ] 246 | * } 247 | * 248 | */ 249 | effects?: (sources: recycle.Sources) => Observable[] 250 | 251 | /** 252 | * Returns the JSX to be rendered for the component 253 | * 254 | * @param props 255 | * @param state 256 | * @returns {JSX.Element} 257 | * @example 258 | * 259 | * view: (props, state) => 260 | *

    261 | *
    Seconds Elapsed: {state.secondsElapsed}
    262 | *
    Times Clicked: {state.counter}
    263 | * 264 | *
    265 | * 266 | */ 267 | view?: (props: P, state: S) => JSX.Element 268 | } 269 | 270 | interface Recycle { 271 | (params: recycle.Params): React.ComponentClass

    272 | } 273 | } 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recycle", 3 | "version": "3.0.0", 4 | "description": "A functional and reactive library for React", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rm -rf dist && mkdir dist && rm -rf lib && mkdir lib", 8 | "test": "jest", 9 | "test:watch": "jest --watch", 10 | "test:coverage": "jest --coverage", 11 | "prepublish": "npm run build && npm run build:umd && npm run build:umd:min", 12 | "build:umd": "browserify lib/index.js -o dist/recycle.js -s recycle && echo \"recycle = recycle.default;\" >> dist/recycle.js", 13 | "build:umd:min": "NODE_ENV=production uglifyjs --compress --mangle -o dist/recycle.min.js -- dist/recycle.js", 14 | "build": "npm run clean && babel src -d ./lib/" 15 | }, 16 | "jest": { 17 | "testMatch": [ 18 | "**/tests/**/*.js?(x)", 19 | "**/src/?(*.)(spec|test).js?(x)" 20 | ] 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/recyclejs/recycle.git" 25 | }, 26 | "author": "Domagoj Kriskovic", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/recyclejs/recycle/issues" 30 | }, 31 | "homepage": "https://recycle.js.org", 32 | "devDependencies": { 33 | "@types/prop-types": "^15.5.2", 34 | "@types/react": "^16.0.12", 35 | "babel-cli": "^6.18.0", 36 | "babel-plugin-add-module-exports": "^0.2.1", 37 | "babel-plugin-syntax-jsx": "^6.18.0", 38 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 39 | "babel-plugin-transform-react-jsx": "^6.24.1", 40 | "babel-preset-es2015": "^6.18.0", 41 | "babel-register": "^6.18.0", 42 | "babelify": "^7.3.0", 43 | "browserify": "^13.3.0", 44 | "classnames": "^2.2.5", 45 | "css-loader": "^0.25.0", 46 | "enzyme": "^2.8.2", 47 | "eslint": "^3.9.1", 48 | "eslint-config-standard": "^6.2.1", 49 | "eslint-config-standard-react": "^4.2.0", 50 | "eslint-plugin-promise": "^3.3.2", 51 | "eslint-plugin-react": "^6.7.1", 52 | "eslint-plugin-standard": "^2.0.1", 53 | "gitbook-cli": "^2.3.0", 54 | "jest": "^20.0.1", 55 | "prop-types": "^15.0.0", 56 | "react": "^15.0.0", 57 | "react-dom": "^15.5.4", 58 | "react-router": "^3.0.0", 59 | "react-test-renderer": "^15.5.4", 60 | "redux": "^3.6.0", 61 | "rxjs": "^5.0.0", 62 | "style-loader": "^0.13.1", 63 | "uglify-js": "^2.7.5" 64 | }, 65 | "peerDependencies": { 66 | "prop-types": "15.x || 16.x", 67 | "react": "15.x || 16.x", 68 | "rxjs": "5.x" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import forceArray from './forceArray' 3 | import shallowClone from './shallowClone' 4 | import customRxOperators from './customRxOperators' 5 | import _makeUpdateNodeStreams from './updateNodeStreams' 6 | import _makeRegisterListeners from './registerListeners' 7 | import _makeCustomCreateElement from './customCreateElement' 8 | 9 | export default (React, Rx) => function recycle (component) { 10 | const customCreateElement = _makeCustomCreateElement(Rx) 11 | const registerListeners = _makeRegisterListeners(Rx) 12 | const updateNodeStreams = _makeUpdateNodeStreams(Rx) 13 | const originalCreateElement = React.createElement 14 | customRxOperators(Rx) 15 | 16 | class RecycleComponent extends React.Component { 17 | componentWillMount () { 18 | this.listeners = [] 19 | this.nodeStreams = [] 20 | 21 | this.sources = { 22 | select: registerListeners(this.listeners, 'tag'), 23 | selectClass: registerListeners(this.listeners, 'class'), 24 | selectId: registerListeners(this.listeners, 'id'), 25 | lifecycle: new Rx.Subject(), 26 | state: new Rx.Subject(), 27 | props: new Rx.Subject() 28 | } 29 | 30 | this.componentState = {...component.initialState} 31 | 32 | // create redux store stream 33 | if (this.context && this.context.store) { 34 | const store = this.context.store 35 | this.sources.store = new Rx.BehaviorSubject(store.getState()) 36 | store.subscribe(() => { 37 | this.sources.store.next(store.getState()) 38 | }) 39 | } 40 | 41 | // dispatch events to redux store 42 | if (component.dispatch && this.context && this.context.store) { 43 | const events$ = Rx.Observable.merge(...forceArray(component.dispatch(this.sources))) 44 | this.__eventsSubsription = events$.subscribe((a) => { 45 | this.context.store.dispatch(a) 46 | }) 47 | } 48 | 49 | // handling component state with update() stream 50 | this.setState(this.componentState) 51 | if (component.update) { 52 | const state$ = Rx.Observable.merge(...forceArray(component.update(this.sources))) 53 | this.__stateSubsription = state$.subscribe(newVal => { 54 | if (this.__componentMounted) { 55 | this.componentState = shallowClone(newVal.reducer(this.componentState, newVal.event)) 56 | } else { 57 | this.componentState = newVal.reducer(this.componentState, newVal.event) 58 | } 59 | this.setState(this.componentState) 60 | }) 61 | } 62 | 63 | if (component.effects) { 64 | const effects$ = Rx.Observable.merge(...forceArray(component.effects(this.sources))) 65 | this.__effectsSubsription = effects$.subscribe(function () { 66 | // intentionally empty 67 | }) 68 | } 69 | } 70 | 71 | componentDidMount () { 72 | this.__componentMounted = true 73 | this.sources.lifecycle.next('componentDidMount') 74 | } 75 | 76 | componentDidUpdate () { 77 | updateNodeStreams(this.listeners, this.nodeStreams) 78 | this.sources.state.next(this.componentState) 79 | this.sources.props.next(this.props) 80 | this.sources.lifecycle.next('componentDidUpdate') 81 | } 82 | 83 | componentWillUnmount () { 84 | this.sources.lifecycle.next('componentWillUnmount') 85 | if (this.__stateSubsription) { 86 | this.__stateSubsription.unsubscribe() 87 | } 88 | if (this.__eventsSubsription) { 89 | this.__eventsSubsription.unsubscribe() 90 | } 91 | if (this.__effectsSubsription) { 92 | this.__effectsSubsription.unsubscribe() 93 | } 94 | } 95 | 96 | render () { 97 | this.nodeStreams = [] 98 | React.createElement = customCreateElement(this.listeners, this.nodeStreams, originalCreateElement) 99 | const view = component.view(this.props, this.componentState) 100 | React.createElement = originalCreateElement 101 | 102 | updateNodeStreams(this.listeners, this.nodeStreams) 103 | this.sources.state.next(this.componentState) 104 | this.sources.props.next(this.props) 105 | 106 | return view 107 | } 108 | } 109 | 110 | RecycleComponent.contextTypes = { 111 | store: PropTypes.object 112 | } 113 | RecycleComponent.propTypes = component.propTypes 114 | RecycleComponent.displayName = component.displayName 115 | 116 | return RecycleComponent 117 | } 118 | -------------------------------------------------------------------------------- /src/customCreateElement.js: -------------------------------------------------------------------------------- 1 | import getNodeSelectors from './getNodeSelectors' 2 | 3 | const customCreateElement = Rx => (listeners, nodeStreams, originalCreateElement) => function () { 4 | const possibleSelectors = getNodeSelectors(arguments['0'], arguments['1']) 5 | 6 | possibleSelectors.forEach(({ selectorType, selector }) => { 7 | listeners 8 | .filter(ref => ref.selector === selector) 9 | .filter(ref => ref.selectorType === selectorType) 10 | .forEach(registredRef => { 11 | let ref = { 12 | selector, 13 | selectorType, 14 | event: registredRef.event 15 | } 16 | if (!arguments['1']) { 17 | arguments['1'] = {} 18 | } 19 | if (typeof arguments['1'][ref.event] === 'function') { 20 | ref.stream = new Rx.Subject() 21 | let customFunction = arguments['1'][ref.event] 22 | arguments['1'][ref.event] = function () { 23 | let event = customFunction.apply(this, arguments) 24 | ref.stream.next(event) 25 | } 26 | } else { 27 | ref.stream = new Rx.Subject() 28 | arguments['1'][ref.event] = function () { 29 | let event = arguments['0'] 30 | ref.stream.next(event) 31 | } 32 | } 33 | nodeStreams.push(ref) 34 | }) 35 | }) 36 | 37 | return originalCreateElement.apply(this, arguments) 38 | } 39 | 40 | export default customCreateElement 41 | -------------------------------------------------------------------------------- /src/customRxOperators.js: -------------------------------------------------------------------------------- 1 | export const reducer = (reducerFn) => (stream) => 2 | stream.map(event => ({ reducer: reducerFn, event })) 3 | 4 | export default Rx => { 5 | if (Rx && Rx.Observable && !Rx.Observable.prototype.reducer) { 6 | Rx.Observable.prototype.reducer = function (reducerFn) { 7 | return reducer(reducerFn)(this) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/forceArray.js: -------------------------------------------------------------------------------- 1 | function forceArray (arr) { 2 | if (!Array.isArray(arr)) return [arr] 3 | return arr 4 | } 5 | 6 | export default forceArray 7 | -------------------------------------------------------------------------------- /src/forceArray.test.js: -------------------------------------------------------------------------------- 1 | /* global expect, it */ 2 | import forceArray from './forceArray' 3 | 4 | it('should return an array', () => { 5 | expect(forceArray(1)).toBeInstanceOf(Array) 6 | }) 7 | 8 | it('should return an array', () => { 9 | expect(forceArray([1, 2, 3])).toBeInstanceOf(Array) 10 | }) 11 | -------------------------------------------------------------------------------- /src/getNodeSelectors.js: -------------------------------------------------------------------------------- 1 | function getNodeSelectors (nodeName, attrs) { 2 | let selectors = [] 3 | 4 | let tag = (typeof nodeName === 'string') ? nodeName : undefined 5 | let id = (attrs) ? attrs.id : undefined 6 | let className = (attrs) ? attrs.className : undefined 7 | let functionSelector = (typeof nodeName === 'function' || typeof nodeName === 'object') ? nodeName : undefined 8 | 9 | if (tag) { 10 | selectors.push({ selector: tag, selectorType: 'tag' }) 11 | } 12 | 13 | if (functionSelector) { 14 | selectors.push({ selector: functionSelector, selectorType: 'tag' }) 15 | } 16 | 17 | if (className) { 18 | let classes = className.split(' ').map(className => ({ selector: className, selectorType: 'class' })) 19 | selectors = selectors.concat(classes) 20 | } 21 | 22 | if (id) { 23 | selectors.push({ selector: id, selectorType: 'id' }) 24 | } 25 | 26 | return selectors 27 | } 28 | 29 | export default getNodeSelectors 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import component from './component' 3 | import { Subject } from 'rxjs/Subject' 4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' 5 | import { Observable } from 'rxjs/Observable' 6 | import 'rxjs/add/observable/merge' 7 | import 'rxjs/add/operator/map' 8 | import 'rxjs/add/operator/mapTo' 9 | import 'rxjs/add/operator/do' 10 | import 'rxjs/add/operator/filter' 11 | import 'rxjs/add/operator/switch' 12 | 13 | const Rx = { 14 | Subject, 15 | Observable, 16 | BehaviorSubject 17 | } 18 | 19 | export const recycle = component(React, Rx) 20 | export { reducer } from './customRxOperators' 21 | export default recycle 22 | -------------------------------------------------------------------------------- /src/registerListeners.js: -------------------------------------------------------------------------------- 1 | const registerListeners = Rx => (listeners, selectorType) => selector => ({ 2 | addListener: event => { 3 | let ref = listeners 4 | .filter(ref => ref.selector === selector) 5 | .filter(ref => ref.selectorType === selectorType) 6 | .filter(ref => ref.event === event)[0] 7 | 8 | if (!ref) { 9 | ref = { 10 | selector, 11 | selectorType, 12 | event, 13 | stream: new Rx.Subject() 14 | } 15 | listeners.push(ref) 16 | } 17 | 18 | return ref.stream.switch() 19 | } 20 | }) 21 | 22 | export default registerListeners 23 | -------------------------------------------------------------------------------- /src/shallowClone.js: -------------------------------------------------------------------------------- 1 | function shallowClone (data) { 2 | if (Array.isArray(data)) { 3 | return [...data] 4 | } else if (typeof data === 'object') { 5 | return {...data} 6 | } 7 | return data 8 | } 9 | 10 | export default shallowClone 11 | -------------------------------------------------------------------------------- /src/shallowClone.test.js: -------------------------------------------------------------------------------- 1 | /* global expect, it */ 2 | import shallowClone from './shallowClone' 3 | 4 | it('should create shallow clone', () => { 5 | let a = { 6 | firstLevel: 1 7 | } 8 | let b = { 9 | firstLevel: 2, 10 | second: a 11 | } 12 | let c = shallowClone(b) 13 | 14 | expect(c).not.toBe(b) 15 | expect(c.second).toBe(b.second) 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/updateNodeStreams.js: -------------------------------------------------------------------------------- 1 | export default Rx => function updateNodeStreams (listeners, nodeStreams) { 2 | listeners.forEach(regRef => { 3 | const streams = nodeStreams 4 | .filter(ref => ref.selector === regRef.selector) 5 | .filter(ref => ref.selectorType === regRef.selectorType) 6 | .filter(ref => ref.event === regRef.event) 7 | .map(ref => ref.stream) 8 | 9 | if (streams.length) { 10 | regRef.stream.next((streams.length === 1) ? streams[0] : Rx.Observable.merge(...streams)) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | // functional tests 2 | /* global expect, it */ 3 | import recycle from '../src' 4 | import React from 'react' 5 | import {shallow} from 'enzyme' 6 | 7 | it('should change label', () => { 8 | const CheckboxWithLabel = recycle({ 9 | initialState: { isChecked: false }, 10 | 11 | update (sources) { 12 | return [ 13 | sources.select('input') 14 | .addListener('onChange') 15 | .reducer(function (state) { 16 | state.isChecked = !state.isChecked 17 | return state 18 | }) 19 | ] 20 | }, 21 | 22 | view (props, state) { 23 | return ( 24 | 31 | ) 32 | } 33 | }) 34 | 35 | const checkbox = shallow( 36 | 37 | ) 38 | 39 | expect(checkbox.text()).toEqual('Off') 40 | checkbox.find('input').simulate('change') 41 | expect(checkbox.text()).toEqual('On') 42 | }) 43 | 44 | --------------------------------------------------------------------------------