├── .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 | [](https://gitter.im/recyclejs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | [](https://www.npmjs.com/package/recycle) 3 | [](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 |
6 | Show:
7 | {" "}
8 |
{ 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 (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 | ) => Observable) => 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) => Observable