/node_modules/babel-jest",
26 | "testFileExtensions": [
27 | "es6",
28 | "js",
29 | "jsx"
30 | ],
31 | "moduleFileExtensions": [
32 | "js",
33 | "json",
34 | "es6",
35 | "jsx"
36 | ]
37 | },
38 | "dependencies": {
39 | "classnames": "^2.2.0",
40 | "transdux": "^0.1.0",
41 | "react": "^0.14.2",
42 | "react-dom": "^0.14.2"
43 | },
44 | "devDependencies": {
45 | "babel": "^6.1.18",
46 | "babel-plugin-transform-react-jsx": "^6.1.18",
47 | "babel-preset-es2015": "^6.1.18",
48 | "babel-jest": "^6.0.0",
49 | "jest-cli": "^0.7.0",
50 | "babelify": "^7.2.0",
51 | "browserify": "^12.0.1",
52 | "ecstatic": "^1.3.1",
53 | "uglify-js": "^2.6.1",
54 | "watchify": "^3.6.1"
55 | },
56 | "author": "Jichao Ouyang",
57 | "license": "ISC",
58 | "babel": {
59 | "presets": [
60 | "es2015"
61 | ],
62 | "plugins": [
63 | "transform-react-jsx"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/examples/todomvc/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Template • TodoMVC
7 |
8 |
9 |
10 |
11 |
12 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/todomvc/src/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Header from './components/Header';
3 | import MainSection from './components/MainSection';
4 | import {render} from 'react-dom';
5 | import Transdux from 'transdux'
6 |
7 | class App extends Component {
8 | render(){
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 | }
17 |
18 | render(
19 |
20 |
21 |
22 | , document.getElementById('app'));
23 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classnames from 'classnames'
3 | import MainSection from './MainSection'
4 | import {TxMixin} from 'transdux'
5 | const FILTER_TITLES = {
6 | 'SHOW_ALL': 'All',
7 | 'SHOW_ACTIVE': 'Active',
8 | 'SHOW_COMPLETED': 'Completed'
9 | }
10 |
11 | let Footer = React.createClass({
12 | mixins: [TxMixin],
13 | renderTodoCount() {
14 | const { activeCount } = this.props
15 | const itemWord = activeCount === 1 ? 'item' : 'items'
16 |
17 | return (
18 |
19 | {activeCount || 'No'} {itemWord} left
20 |
21 | )
22 | },
23 |
24 | renderFilterLink(filter) {
25 | const title = FILTER_TITLES[filter]
26 | const { filter: selectedFilter, onShow } = this.props
27 |
28 | return (
29 | this.dispatch(MainSection, 'show', filter)}>
32 | {title}
33 |
34 | )
35 | },
36 |
37 | renderClearButton() {
38 | const { completedCount } = this.props
39 | if (completedCount > 0) {
40 | return (
41 |
45 | )
46 | }
47 | },
48 |
49 | render() {
50 | return (
51 |
62 | )
63 | },
64 | });
65 |
66 |
67 | export default Footer
68 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import TodoTextInput from './TodoTextInput'
3 | let Header = React.createClass({
4 | render() {
5 | return (
6 |
11 | )
12 | },
13 | })
14 |
15 | export default Header
16 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/MainSection.action.js:
--------------------------------------------------------------------------------
1 | const actions = {
2 | complete(msg, state){
3 | return {
4 | todos:state.todos.map(todo=>{
5 | if(todo.id==msg.id)
6 | todo.completed = !todo.completed
7 | return todo
8 | })
9 | }
10 | },
11 | show(msg,state){
12 | switch(msg){
13 | case('SHOW_ALL'):
14 | return {filter: _=>_}
15 | case('SHOW_ACTIVE'):
16 | return {filter: todos=>todos.filter(todo=>!todo.completed)}
17 | case('SHOW_COMPLETED'):
18 | return {filter: todos=>todos.filter(todo=>todo.completed)}
19 | }
20 | },
21 | clear(msg,state){
22 | return {
23 | todos: state.todos.filter(todo=>todo.completed==false)
24 | }
25 | },
26 | add(msg, state){
27 | let todos = state.todos
28 | todos.unshift({id:todos.length+1, text:msg, completed:false})
29 | return {
30 | todos: todos
31 | }
32 | },
33 | edit(msg, state){
34 | return {
35 | todos: state.todos.map(todo=>{
36 | if(todo.id == msg.id){todo.text=msg.text}
37 | return todo;
38 | })
39 | }
40 | },
41 | delete(msg, state){
42 | return {
43 | todos: state.todos.filter(todo=>{
44 | return todo.id!=msg.id
45 | })
46 | }
47 | }
48 | }
49 | export default actions
50 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/MainSection.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import TodoItem from './TodoItem'
3 | import Footer from './Footer'
4 | import {TxMixin} from 'transdux'
5 | import actions from './MainSection.action'
6 | const todos = [{
7 | text: 'Dont Use Redux',
8 | completed: false,
9 | id: 0
10 | },{
11 | text: 'Use transdux',
12 | completed: false,
13 | id: 1
14 | }];
15 |
16 | let MainSection = React.createClass({
17 | mixins: [TxMixin],
18 | getInitialState(){
19 | return {
20 | todos: todos,
21 | filter: _=>_
22 | }
23 | },
24 | componentDidMount(){
25 | this.bindActions(actions)
26 | },
27 |
28 | handleShow(filter) {
29 | this.setState({ filter })
30 | },
31 |
32 | renderToggleAll(completedCount) {
33 | const { todos } = this.state
34 | if (todos.length > 0) {
35 | return (
36 |
40 | )
41 | }
42 | },
43 |
44 | renderFooter(completedCount) {
45 | const { todos } = this.state
46 | const activeCount = todos.length - completedCount;
47 |
48 | if (todos.length) {
49 | return (
50 |
51 | )
52 | }
53 | },
54 |
55 | render() {
56 | const { actions } = this.props
57 | const {todos} = this.state
58 | const completedCount = todos.reduce((count, todo) =>
59 | todo.completed ? count + 1 : count,
60 | 0
61 | );
62 |
63 | const filteredTodos = this.state.filter(todos);
64 | return (
65 |
66 | {this.renderToggleAll(completedCount)}
67 |
68 | {filteredTodos.map(todo =>
69 |
70 | )}
71 |
72 | {this.renderFooter(completedCount)}
73 |
74 | )
75 | },
76 | });
77 |
78 | export default MainSection
79 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/TodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import TodoTextInput from './TodoTextInput'
4 | import MainSection from './MainSection'
5 | import {mixin} from 'transdux'
6 |
7 | let actions = {
8 | save(msg, state,props) {
9 | if(msg.id!=props.todo.id) return
10 | return {
11 | editing: false
12 | }
13 | }
14 | }
15 |
16 |
17 | class TodoItem extends React.Component {
18 | constructor(props){
19 | super(props);
20 | this.state = {editing:false};
21 | }
22 | render() {
23 | const { todo } = this.props
24 |
25 | let element
26 | if (this.state.editing) {
27 | element = (
28 |
32 | )
33 | } else {
34 | element = (
35 |
36 | this.dispatch(MainSection, 'complete',{id:todo.id})} />
40 |
43 |
46 | )
47 | }
48 |
49 | return (
50 |
54 | {element}
55 |
56 | )
57 | }
58 | }
59 |
60 |
61 | export default mixin(TodoItem, actions, _=>{console.log('hehe'); return _})
62 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/TodoTextInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import {TxMixin} from 'transdux'
4 | import MainSection from './MainSection'
5 | import TodoItem from './TodoItem'
6 | let TodoTextInput = React.createClass({
7 | mixins: [TxMixin],
8 | getInitialState(){
9 | return {
10 | text: this.props.text || ''
11 | }
12 | },
13 |
14 | handleSubmit(e) {
15 | const text = e.target.value.trim()
16 | let msg = {id:this.props.itemid,text:text}
17 | if (e.which === 13) {
18 | if (this.props.newTodo) {
19 | this.dispatch(MainSection, 'add', text)
20 | this.setState({ text: '' })
21 | }
22 | this.handleBlur(e)
23 | }
24 | },
25 |
26 | handleChange(e) {
27 | this.setState({ text: e.target.value })
28 | },
29 |
30 | handleBlur(e) {
31 | if (!this.props.newTodo) {
32 | let msg = {id:this.props.itemid,text:e.target.value}
33 | if (msg.text.length === 0) {
34 | this.dispatch(MainSection, 'delete', msg)
35 | } else {
36 | this.dispatch(MainSection, 'edit', msg)
37 | }
38 | this.dispatch(TodoItem, 'save', msg)
39 | }
40 | },
41 |
42 | render() {
43 | return (
44 |
56 | )
57 | },
58 | });
59 |
60 | export default TodoTextInput
61 |
--------------------------------------------------------------------------------
/examples/todomvc/src/components/__tests__/TodoItem-spec.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-react/transdux/d7f4014ae722c74f97b072ebffb3d48bba71bbff/examples/todomvc/src/components/__tests__/TodoItem-spec.jsx
--------------------------------------------------------------------------------
/lib/__tests__/transdux-test.jsx:
--------------------------------------------------------------------------------
1 | jest.dontMock('../transdux.js');
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import TestUtils from 'react-addons-test-utils';
5 | import uuid from 'uuid';
6 |
7 | const {default:Transdux, TxMixin} = require('../transdux');
8 |
9 | describe('transdux', () => {
10 | describe('todo dispatch', ()=>{
11 | let todolist;
12 | let Todo = React.createClass({
13 | render(){
14 | return {this.props.todo.text}
15 | }
16 | });
17 | let TodoList = React.createClass({
18 | mixins: [TxMixin],
19 | getInitialState(){
20 | return {
21 | todos: [
22 | {id:1, text:'hehe', done:false},
23 | {id:2, text:'heheda', done:false},
24 | ]
25 | }
26 | },
27 | componentDidMount(){
28 | this.bindActions({
29 | complete(msg, state){
30 | return {
31 | todos: state.todos.map(todo => {
32 | if(todo.id == msg){
33 | todo.done =! todo.done
34 | }
35 | return todo
36 | })
37 | }
38 | }
39 | })
40 | },
41 | render(){
42 | let todos = this.state.todos.map(todo => {
43 | return
44 | });
45 | return {todos}
46 | }
47 | });
48 | let Buttons = React.createClass({
49 | mixins: [TxMixin],
50 | render(){
51 | return (
52 |
53 |
54 |
55 |
)
56 | }
57 | });
58 |
59 | beforeEach(()=>{
60 | todolist = TestUtils.renderIntoDocument(
61 |
62 |
63 |
64 |
65 | )
66 | });
67 |
68 | it('click complete buttom will complete', () => {
69 | expect(TestUtils.scryRenderedComponentsWithType(todolist, Todo)[0].props.todo.done).toBe(false);
70 | TestUtils.Simulate.click(TestUtils.findRenderedDOMComponentWithClass(todolist, 'btn-complete'));
71 | jest.runAllTimers();
72 | expect(TestUtils.scryRenderedComponentsWithType(todolist, Todo)[0].props.todo.done).toBe(true);
73 | });
74 | })
75 | });
76 |
--------------------------------------------------------------------------------
/lib/transdux.js:
--------------------------------------------------------------------------------
1 | import {async,map} from 'con.js/async';
2 | const {sub,observe,put,put$,chan,pub,pipeline} = async;
3 | import React from 'react';
4 | import uuid from 'uuid';
5 |
6 | function genUuid(reactClass){
7 | reactClass.uuid = reactClass.uuid || uuid.v4();
8 | return reactClass.uuid;
9 | }
10 | const id = _=>_;
11 |
12 | export const TxMixin = {
13 | contextTypes: {
14 | transduxChannel: React.PropTypes.object,
15 | transduxPublication: React.PropTypes.object,
16 | },
17 | bindActions(actions, imm=id, unimm=id) {
18 | for(let name in actions){
19 | let tx = map((msg)=>{
20 | this.setState((prevState, props)=>{
21 | return unimm(actions[name].call(this, msg.value, imm(prevState), imm(props)));
22 | });
23 | return this.state
24 | });
25 | let actionChan = chan(32,tx);
26 | sub(this.context.transduxPublication, genUuid(this.constructor)+name, actionChan);
27 | observe(actionChan, (newstate)=>{});
28 | }
29 | },
30 | dispatch(where, how, what) {
31 | put(this.context.transduxChannel,
32 | {action:genUuid(where)+how,
33 | value:what})
34 | .then(id,_=>console.log('[Error] dispatching:'+what))
35 | }
36 | }
37 |
38 | export function mixin(reactClass, ...actions) {
39 | reactClass.contextTypes = TxMixin.contextTypes;
40 | for (let name in TxMixin) {
41 | if(name!='contextTypes')
42 | reactClass.prototype[name] = TxMixin[name];
43 | }
44 | let oldMountFunc = reactClass.prototype.componentDidMount;
45 | reactClass.prototype.componentDidMount = function(){
46 | oldMountFunc && oldMountFunc();
47 | this.bindActions(...actions);
48 | }
49 | return reactClass
50 | }
51 |
52 | const Transdux = React.createClass({
53 | childContextTypes: {
54 | transduxChannel: React.PropTypes.object,
55 | transduxPublication: React.PropTypes.object,
56 | },
57 | getChildContext(){
58 | let inputchan = chan();
59 | return {
60 | transduxChannel: inputchan,
61 | transduxPublication: pub(inputchan, _=>_['action']),
62 | }
63 | },
64 | render(){
65 | return {this.props.children}
66 | }
67 | });
68 |
69 | export default Transdux;
70 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transdux",
3 | "version": "0.1.1",
4 | "description": "",
5 | "main": "transdux.js",
6 | "scripts": {
7 | "build": "cat LICENSE.txt > transdux.js && ./node_modules/.bin/babel lib/transdux.js >> transdux.js",
8 | "test": "jest",
9 | "prepublish": "npm run build"
10 | },
11 | "jest": {
12 | "scriptPreprocessor": "/node_modules/babel-jest",
13 | "testPathDirs": [
14 | "lib"
15 | ],
16 | "testFileExtensions": [
17 | "es6",
18 | "js",
19 | "jsx"
20 | ],
21 | "moduleFileExtensions": [
22 | "js",
23 | "json",
24 | "es6",
25 | "jsx"
26 | ],
27 | "unmockedModulePathPatterns": [
28 | "/node_modules/react",
29 | "/node_modules/react-dom",
30 | "/node_modules/react-addons-test-utils",
31 | "/node_modules/fbjs",
32 | "/node_modules/uuid",
33 | "/node_modules/con.js",
34 | "/node_modules/fbjs"
35 | ]
36 | },
37 | "dependencies": {
38 | "con.js": "^0.5.3",
39 | "react": "^0.14.3",
40 | "uuid": "^2.0.1"
41 | },
42 | "devDependencies": {
43 | "babel": "^6.1.18",
44 | "babel-cli": "^6.2.0",
45 | "babel-jest": "^6.0.0",
46 | "babel-plugin-transform-react-jsx": "^6.1.18",
47 | "babel-preset-es2015": "^6.1.18",
48 | "babelify": "^7.2.0",
49 | "browserify": "^12.0.1",
50 | "ecstatic": "^1.3.1",
51 | "immutable": "^3.7.5",
52 | "jest-cli": "^0.7.0",
53 | "react-addons-test-utils": "^0.14.3",
54 | "react-dom": "^0.14.3",
55 | "redux": "^3.0.4",
56 | "uglify-js": "^2.6.1",
57 | "watchify": "^3.6.1"
58 | },
59 | "author": "Jichao Ouyang",
60 | "license": "MIT",
61 | "babel": {
62 | "presets": [
63 | "es2015"
64 | ],
65 | "plugins": [
66 | "transform-react-jsx"
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------