├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── build.sbt ├── ci └── npm-login.sh ├── circle.yml ├── codecov.yml ├── dist └── .emtpy ├── docs └── src │ └── main │ ├── resources │ └── microsite │ │ ├── css │ │ └── override.css │ │ └── img │ │ ├── first_icon.png │ │ ├── first_icon2x.png │ │ ├── futurama_June_22__2016_at_0120AM.gif │ │ ├── futurama_September_11__2016_at_0545AM (2).gif │ │ ├── jumbotron_pattern.png │ │ ├── jumbotron_pattern2x.png │ │ ├── navbar_brand.png │ │ ├── navbar_brand2x.png │ │ ├── second_icon.png │ │ ├── second_icon2x.png │ │ ├── sidebar_brand.png │ │ ├── sidebar_brand2x.png │ │ ├── third_icon.png │ │ └── third_icon2x.png │ └── tut │ ├── API.md │ ├── CNAME │ ├── Examples.md │ ├── FAQ.md │ ├── FRP-Best-Practice.md │ ├── Fantasy.md │ ├── Get-Started.md │ ├── Home.md │ ├── Monadic.md │ ├── Notation.md │ ├── Performance.md │ ├── React-Most-函数式最佳实践.md │ ├── _Footer.md │ ├── _Sidebar.md │ ├── circle.yml │ ├── examples │ ├── example.tsx │ ├── index.html │ ├── index.md │ └── index.org │ ├── flatMap.md │ ├── index.md │ ├── typeclass.md │ ├── typeclass.org │ ├── zh.md │ ├── 教程.md │ └── 范特西.md ├── examples ├── README.md ├── alacarte │ ├── app.js │ ├── app.jsx │ └── index.html ├── bmi-calc │ ├── app.js │ ├── app.tsx │ └── index.html ├── counter │ ├── README.md │ ├── app.js │ ├── app.jsx │ └── index.html ├── frp-counter │ ├── app.js │ ├── app.jsx │ └── index.html ├── index.md ├── package.json ├── todomvc │ ├── README.md │ ├── __mocks__ │ │ └── rest.js │ ├── app.js │ ├── app.tsx │ ├── components │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── MainSection.tsx │ │ ├── TodoItem.tsx │ │ ├── TodoTextInput.tsx │ │ ├── __tests__ │ │ │ └── MainSection-spec.jsx │ │ └── interfaces.ts │ ├── index.html │ ├── intent.ts │ ├── todos.json │ └── tsconfig.json ├── tsconfig.json ├── type-n-search │ ├── README.md │ ├── app.js │ ├── app.jsx │ ├── index.html │ └── src │ │ ├── transducer.jsx │ │ └── undo.jsx └── yarn.lock ├── package.json ├── project ├── build.properties └── plugins.sbt ├── src ├── __tests__ │ ├── fantasy-test.ts │ ├── fantasyx-test.ts │ └── xtest.tsx ├── fantasy │ ├── fantasyx.ts │ ├── index.ts │ ├── interfaces.ts │ ├── state.ts │ ├── streamT.ts │ ├── typeclasses │ │ ├── applicative.ts │ │ ├── apply.ts │ │ ├── cartesian.ts │ │ ├── flatmap.ts │ │ ├── functor.ts │ │ ├── id.ts │ │ ├── index.ts │ │ ├── monad.ts │ │ ├── semigroup.ts │ │ └── traversable.ts │ └── xstream.ts ├── index.ts ├── interfaces.ts ├── x.ts ├── xclass.ts ├── xs │ ├── array.ts │ ├── index.ts │ ├── most.ts │ └── rx.ts └── xtests │ ├── index.ts │ ├── most.ts │ ├── rx.ts │ └── util.ts ├── test └── test.js ├── tsconfig.common.json ├── tsconfig.examples.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [Makefile] 5 | indent_style = tab 6 | indent_size = 2 7 | 8 | [*.{ts,tsx}] 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | ##########idea 9 | .idea/ 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 32 | node_modules 33 | 34 | lib 35 | 36 | # Created by https://www.gitignore.io/api/sbt 37 | 38 | ### SBT ### 39 | # Simple Build Tool 40 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 41 | 42 | dist/* 43 | target/ 44 | lib_managed/ 45 | src_managed/ 46 | project/boot/ 47 | project/plugins/project/ 48 | .history 49 | .cache 50 | .lib/ 51 | 52 | # End of https://www.gitignore.io/api/sbt 53 | /examples 54 | /docs/**/example.js 55 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 Jichao Ouyang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docsdir = ./docs/**/* 2 | bin = ./node_modules/.bin 3 | 4 | test: unit integrate 5 | 6 | build: lib/**/*.js 7 | 8 | lib/**/*.js: src/**/*.ts 9 | $(bin)/tsc 10 | 11 | lib/%.js: src/%.ts 12 | $(bin)/tsc 13 | 14 | all: test dist 15 | 16 | .PHONY: test build unit integrate dist docs docs/publish 17 | 18 | unit: build 19 | yarn test 20 | 21 | integrate: build test/*.js docs/src/main/tut/examples/example.js 22 | $(bin)/mocha test/test.js 23 | 24 | docs/src/main/tut/examples/example.js: docs/src/main/tut/examples/example.tsx 25 | $(bin)/browserify -p [tsify -p tsconfig.examples.json] docs/src/main/tut/examples/example.tsx -o docs/src/main/tut/examples/example.js 26 | 27 | watch/example: docs/src/main/tut/examples/example.tsx 28 | $(bin)/watchify -p [tsify -p tsconfig.examples.json] -t envify docs/src/main/tut/examples/example.tsx -dv -o docs/src/main/tut/examples/example.js 29 | 30 | dist: dist/xreact.min.js dist/xreact-most.min.js dist/xreact-rx.min.js 31 | 32 | dist/xreact.js: lib/index.js dist/xreact-most.js dist/xreact-rx.js 33 | env NODE_ENV=production $(bin)/browserify -t browserify-shim -t envify -x ./lib/xs $< -s xreact -o $@ 34 | 35 | dist/xreact-%.js: lib/xs/%.js 36 | env NODE_ENV=production $(bin)/browserify -t browserify-shim -t envify -r ./lib/xs $< -o $@ 37 | 38 | dist/%.min.js: dist/%.js 39 | env NODE_ENV=production $(bin)/uglifyjs -c dead_code $(basename $(basename $@)).js -o $@ 40 | 41 | docs: $(docsdir) 42 | sbt "project docs" makeMicrosite 43 | 44 | docs/publish: $(docsdir) 45 | sbt "project docs" publishMicrosite 46 | 47 | clean: 48 | rm -rf lib docs/src/main/tut/examples/example.js dist/* 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xReact 2 | 3 | xReact is a Functional Reactive State Wrapper for React Components. Data flow in xReact is observable and unidirectional. 4 | 5 | > formerly know as react-most, renamed so because mostjs is not madatory anymore. 6 | 7 | [![CircleCI](https://img.shields.io/circleci/project/github/reactive-react/xreact/master.svg)](https://circleci.com/gh/reactive-react/xreact) 8 | [![Join the chat at https://gitter.im/jcouyang/react-most](https://badges.gitter.im/jcouyang/react-most.svg)](https://gitter.im/jcouyang/react-most?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | [![](https://img.shields.io/github/stars/reactive-react/xreact.svg?label=Star)](https://github.com/reactive-react/xreact) 10 | [![codecov](https://codecov.io/gh/reactive-react/xreact/branch/master/graph/badge.svg)](https://codecov.io/gh/reactive-react/xreact) 11 | [![npm](https://img.shields.io/npm/v/xreact.svg)](https://www.npmjs.com/package/xreact) 12 | [![greenkeeper.io](https://badges.greenkeeper.io/reactive-react/xreact.svg)](https://greenkeeper.io) 13 | 14 | ![](https://www.evernote.com/l/ABet-_q4zTxGQrpnD0lwf_An5z9FvAQOvNEB/image.png) 15 | 16 | ## [Get Started](https://xreact.oyanglul.us) 17 | 18 | xReact works for both TypeScript and CMD JS, to install xReact simply use yarn or npm: 19 | 20 | ``` 21 | npm install xreact --save 22 | # or 23 | yarn add xreact 24 | ``` 25 | 26 | 27 | - Come from redux? :point_right: 28 | - Come from fantasy land? :rainbow: 29 | 30 | ## Documentation 31 | 32 | All xReact information and documentation is available on [the website](http://xreact.oyanglul.us/). 33 | 34 | And... we have a list of [frequently-asked questions](https://xreact.oyanglul.us/FAQ.html). 35 | 36 | Our goal is to have clear and comprehensive documentation. If you notice problems, omissions, or errors, please [let us know](https://github.com/reactive-react/xreact/issues), or PR is more than welcome. 37 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val docs = project 2 | .enablePlugins(MicrositesPlugin) 3 | .settings( 4 | micrositeName := "xReact", 5 | micrositeDescription := "Reactive x React = xReact", 6 | micrositeAuthor := "Jichao Ouyang", 7 | micrositeHomepage := "https://xreact.oyanglul.us/", 8 | micrositeOrganizationHomepage := "https://oyanglul.us", 9 | micrositeTwitter := "@oyanglulu", 10 | micrositeGithubOwner := "reactive-react", 11 | micrositeGithubRepo := "xreact", 12 | micrositeDocumentationUrl := "/Get-Started.html", 13 | micrositeGitterChannel := true, 14 | micrositeGitterChannelUrl := "jcouyang/react-most" 15 | ) 16 | -------------------------------------------------------------------------------- /ci/npm-login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset 3 | set -o errexit 4 | 5 | npm login < {sink$: Stream, actions: Map String Function}) -> ReactClass -> ReactClass 5 | connect:: (Stream -> Object -> {sink$: Stream, actions: Map String Function}) -> ReactClass -> ReactClass 6 | connect(dataFlow[, options]) 7 | ``` 8 | 9 | 10 | `connect` mean to connect some behavior to a React Component, you can think it as a HOC wrapper or decorator, 11 | ### return `ReactClass -> ReactClass` 12 | it return a function map a React Class to a new React Class, with all behavior define in dataFlow 13 | 14 | ```js 15 | import {connect} from 'react-most' 16 | 17 | class TodoItem extends React.Component { 18 | ... 19 | } 20 | export default connect(function(intent$){ 21 | let sink$ = intent$.filter(...).map(....) 22 | return {sink$} 23 | })(TodoItem) 24 | ``` 25 | 26 | ### parameter dataFlow `Stream -> Object -> {sink$: Stream, actions: {name: Function}}` 27 | 28 | data flow is user define flow or behavior for intent stream, must return a object contains `actions` or `sinks` 29 | 30 | ```js 31 | let RxCounter = connect(function(intent$){ 32 | let addSink$ = intent$.filter(x=>x.type=='add').map(({increment})=>state=>({value: state.value+increment})) 33 | return { 34 | add: increment=>({type: 'add', increment}), // <-- define a action, Counter can trigger a action by invoke props.actions.add() 35 | addSink$, // <-- define a behavior when someone intent to trigger a "add" action 36 | } 37 | })(Counter); 38 | ``` 39 | a sink$ must be a state transform stream, in this case it's Stream contains `state=>({value: state.value+increment})` 40 | 41 | you can get current props from state transformer as well 42 | ```js 43 | (state, props)=>({value: state.value+props.increment}) 44 | ``` 45 | 46 | #### parameters 47 | - parameter `intent$` will be given by [Most Provider](#Most) 48 | - parameter `initProps` is the prop at the time component is created, this maybe different from `props` parameter in transformer function, which is the props when the transformer function is called. 49 | 50 | ### parameter: options 51 | options that you can give to the connect function: 52 | 1. [history](#history) 53 | 54 | ## Most 55 | `Most` provider provide a intent$ stream container, all component inside the provider will share the same intent$ stream. 56 | ```js 57 | import Most from 'react-most' 58 | 59 | 60 | 61 | ``` 62 | 63 | ## History and Time travel [experimental] 64 | ```js 65 | connect(intent$=>[awesome flow], {history:true})(App) 66 | ``` 67 | 68 | or 69 | 70 | ```js 71 | 72 | 73 | ``` 74 | once you connect history to App, you have two extract methods from `props` 75 | 76 | 1. `props.history.backward` 77 | 2. `props.history.forward` 78 | 79 | 80 | 81 | ### Reactive engine [experimental] 82 | if you are Rx user, optionally you can pass a `engine` props into `Most`. 83 | ```js 84 | import Most from 'react-most' 85 | 94 | 95 | 96 | ``` 97 | other reactive lib user can easily proivde you favor engine by simply provide these 3 things: 98 | 99 | 1. `intentStream`: a Steam contains intents 100 | 2. `historyStream`: a Stream contains history 101 | 3. `flatObserve(sinks,func)`: flat Stream of Stream `sinks`, and observe with `func`. 102 | -------------------------------------------------------------------------------- /docs/src/main/tut/CNAME: -------------------------------------------------------------------------------- 1 | xreact.oyanglul.us 2 | -------------------------------------------------------------------------------- /docs/src/main/tut/Examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Examples 4 | section: en 5 | position: 4 6 | --- 7 | 8 | # Counter 9 | 10 | 11 | 12 | ## BMI Calculator 13 | 14 | 15 | 16 | - [Type N Search](https://github.com/reactive-react/react-most/tree/master/examples/type-n-search) [(live)](https://reactive-react.github.io/react-most/examples/type-n-search/public/) 17 | 18 | - [TodoMVC](https://github.com/reactive-react/react-most/tree/master/examples/todomvc) [(live)](https://reactive-react.github.io/react-most/examples/todomvc/public/) 19 | -------------------------------------------------------------------------------- /docs/src/main/tut/FAQ.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: FAQ 4 | section: en 5 | position: 3 6 | --- 7 | 8 | # Frequently Asked Questions 9 | ### How it's different from redux? 10 | 11 | unlike redux, xreact turn FRP to 11 in react, it model problem different 12 | 13 | - "global" intent stream(using redux's word should be intent store) not global state store 14 | - there's not such thing as state store, no state will store anywhere, only state transformations 15 | - FRP lib as your choice, choose any lib your familiar with 16 | 17 | ### How it's different from cycle.js? 18 | 19 | think xreact as a more specify and optimized cycle just for react. 20 | 21 | ### Why not just reduce into a new state(value) just like redux? 22 | 23 | Q: Instead of: 24 | 25 | ```js 26 | const update = intent => { 27 | switch (intent.type) { 28 | case 'inc': 29 | return state => ({ count: state.count + 1 }); 30 | case 'dec': 31 | return state => ({ count: state.count - 1 }); 32 | default: 33 | return _ => _; 34 | } 35 | } 36 | ``` 37 | 38 | why not just simply: 39 | ```js 40 | const update = intent => { 41 | switch (intent.type) { 42 | case 'inc': 43 | return ({ count: state.count + 1 }); 44 | case 'dec': 45 | return ({ count: state.count - 1 }); 46 | default: 47 | return _ => _; 48 | } 49 | } 50 | ``` 51 | 52 | A: https://github.com/reactive-react/xreact/issues/40 53 | 54 | ### Why not global state? 55 | global is state is not scalable, think it as a database, and every component query data from it,however, database are hard to scale, design and maintain. 56 | 57 | instead of making state global, we think a better choice of doing such reversely, just have what you want to do(intent) globally instead. So, every component can just broadcast what it's trying to do, but only focus on how to reduce intent into a state transformation for it self. 58 | 59 | In this case, one component won't need worry about how the global state structure, and just focus on itself. So, components are more modular and decoupled. 60 | 61 | Furher more, it's composable, we can build small x component constructors and compose them at will to create a bigger and powerfult component constructors. It's much easier and flexible by compose small behavior and state into a big component, not destruct a big global state into small components. 62 | -------------------------------------------------------------------------------- /docs/src/main/tut/FRP-Best-Practice.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Functional Best Practice 4 | section: en 5 | --- 6 | 7 | Functional Best Practice in xReact 8 | ================================== 9 | 10 | I'll guide you to some good practice of Functional Programming through the Counter example. 11 | 12 | > It's really helpful even you don't use xreact, the FP idea is common and applicable to anywhere even for redux project. 13 | Usee Union Type to define Intent 14 | ------------------------------- 15 | 16 | union-type is an awesome library to define union type/case class. 17 | 18 | most flux-like library will define Intents using the keyword \`type\` 19 | 20 | ``` javascript 21 | inc: () => ({type: 'inc'}) 22 | ``` 23 | 24 | but union type fit perfectly to define Intent 25 | 26 | `Intent.js` 27 | 28 | ``` javascript 29 | import Type from 'union-type' 30 | export default Type({ 31 | Inc: [] 32 | Dec: [] 33 | }) 34 | ``` 35 | 36 | case Intent, not switch 37 | ----------------------- 38 | 39 | it's like case class in scala, you get a lot of benefit by using union-type Intent 40 | 41 | ``` javascript 42 | import Intent from 'intent' 43 | const counterable = x(intent$ => { 44 | return { 45 | sink$: intent$.map(Intent.case({ 46 | Inc: () => state => ({count: state.count + 1}), 47 | Dec: () => state => ({count: state.count - 1}), 48 | _: () => state => state 49 | })), 50 | actions: { 51 | inc: Intent.Inc, 52 | dec: Intent.Dec, 53 | } 54 | } 55 | }) 56 | ``` 57 | 58 | pattern match case class 59 | ------------------------ 60 | 61 | like scala, union type can also contain values 62 | 63 | ``` javascript 64 | import Type from 'union-type' 65 | export default Type({ 66 | Inc: [Number] 67 | Dec: [Number] 68 | }) 69 | ``` 70 | 71 | if you define Intent constructors, you will be able to destruct them via `case` 72 | 73 | ``` javascript 74 | import Intent from 'intent' 75 | const counterable = x(intent$ => { 76 | return { 77 | sink$: intent$.map(Intent.case({ 78 | Inc: (value) => state => ({count: state.count + value}), 79 | Dec: (value) => state => ({count: state.count - value}), 80 | _: () => state => state 81 | })), 82 | actions: { 83 | inc: Intent.Inc, 84 | dec: Intent.Dec, 85 | } 86 | } 87 | }) 88 | ``` 89 | 90 | lens 91 | ---- 92 | 93 | lens is composable, immutable, functional way to view, update your state 94 | 95 | you can use `lens` implemented by ramda, or `update` in lodash 96 | 97 | ``` javascript 98 | import {lens, over, inc, dec, identity} from 'ramda' 99 | const counterable = x(intent$ => { 100 | let lensCount = lens(prop('count')) 101 | return { 102 | sink$: intent$.map(Intent.case({ 103 | Inc: () => over(lensCount, inc) 104 | Dec: () => over(lensCount, dec), 105 | _: () => identity 106 | })) 107 | } 108 | }) 109 | ``` 110 | 111 | flatMap 112 | ------- 113 | 114 | when the value is async, e.g. promise 115 | 116 | it could be response from rest request, or other async IO 117 | 118 | ``` javascript 119 | import when from 'when' 120 | import {just, from, lens, over, set, inc, dec, identity, compose} from 'ramda' 121 | const counterable = x(intent$ => { 122 | let lensCount = lens(prop('count')) 123 | return { 124 | sink$: intent$.map(Intent.case({ 125 | Inc: () => over(lensCount, inc) 126 | Dec: () => over(lensCount, dec), 127 | _: () => identity 128 | })) 129 | data$: just(0) 130 | .flatMap(compose(from, when)) // <-- when is a async value 131 | .map(set(lensCount)) 132 | } 133 | }) 134 | ``` 135 | 136 | Composable wrapper 137 | ------------------ 138 | 139 | x wrappers are composable, just like functions 140 | 141 | ``` javascript 142 | import Type from 'union-type' 143 | export default Type({ 144 | Inc: [Number], 145 | Dec: [Number], 146 | Double: [], 147 | Half: [] 148 | }) 149 | ``` 150 | 151 | create a new wrapper with some kind of behaviors 152 | 153 | ``` javascript 154 | const doublable = x(intent$ => { 155 | let lensCount = lens(prop('count')) 156 | return { 157 | sink$: intent$.map(Intent.case({ 158 | Double: () => over(lensCount, x=>x*2) 159 | Half: () => over(lensCount, x=>X/2), 160 | _: () => identity, 161 | })) 162 | actions: { 163 | double: Intent.Double, 164 | half: Intent.Half, 165 | } 166 | } 167 | }) 168 | ``` 169 | 170 | compose doublable and increasable 171 | 172 | ``` javascript 173 | const Counter = doublable(increasable(CounterView)) 174 | ``` 175 | 176 | `CounterView` then get both abilities of double/half and inc/dec 177 | 178 | ``` javascript 179 | const CounterView = props => ( 180 |
181 | 182 | 183 | {props.count} 184 | 185 | 186 |
187 | ) 188 | ``` 189 | 190 | now our FRP counter example will become something like [this](https://github.com/reactive-react/xreact/blob/master/examples/frp-counter/app.jsx) 191 | -------------------------------------------------------------------------------- /docs/src/main/tut/Fantasy.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Fantasy 4 | section: en 5 | position: 3 6 | --- 7 | 8 | # XReact Fantasy 9 | 10 | xReact is a Functional library that can integrate FRP lib rxjs or mostjs into react. But there're still too many verbose you need to care while modeling UI components. 11 | 12 | The implement of [Fantasy Land](https://github.com/fantasyland/fantasy-land), which will change the way you model and implement UI entirely. 13 | 14 | > The idea of FantasyX is highly inspired by [flare](http://sharkdp.github.io/purescript-flare/) by purescript 15 | 16 | ## `lift` 17 | 18 | Let's use List as example, what `lift` does is very similar to `map` 19 | 20 | ```js 21 | const f = x => x + 1 22 | [1,2,3].map(f) // => [2,3,4] 23 | ``` 24 | 25 | It simply map `f` to every items in the list. While if we do it another way around: 26 | 27 | ```js 28 | const lf = lift(f) 29 | lf([1,2,3]) // => [2,3,4] 30 | ``` 31 | 32 | Now `lf` can take a list, apply `f` to each item, and return a new list. So `lf` is just a lifted version of `f`. You should notice that both lift and map transform `x => x + 1` which should only able to apply to `Number`, to a function that can apply to `Array` 33 | 34 | We can now (from v2.3.0) lift a normal function(takes value and return value) to a FantasyX level function(take FantasyX and return FantasyX) as well. 35 | 36 | Let's take a look at a really simple example, multiply 2 numbers. 37 | 38 | ```js 39 | // Number -> Number -> Number 40 | function mult(a, b) { 41 | return a * b 42 | } 43 | mult(1, 2) 44 | ``` 45 | 46 | But if we need a React Component that multiply 2 numbers from 2 input boxes, how complicated it could be? 47 | 48 | Now you get simply get a free FantasyX from just any normal function, via `lift`. 49 | 50 | ```js 51 | // FantasyX -> FantasyX -> FantasyX 52 | let xMult = lift2(mult) 53 | ``` 54 | 55 | `mult` need 2 arguments, that's why we use `lift2` here. 56 | 57 | 58 | Now that we have on function `xMult` that can turn 2 FantasyX into one, lets do this: 59 | 60 | ```js 61 | let XMult = xMult(xinput('a'), xinput('b')) 62 | ``` 63 | 64 | and we got a new FantasyX `XMult` with the computation built in. 65 | 66 | `xinput` is an FantasyX abstraction of input box. 67 | 68 | All we have so far was just a FantasyX `XMult` with computation composed inside, and we need a View to display and interact with. Here comes a really simple Stateless Component 69 | 70 | ```js 71 | const View = props => ( 72 |
73 | 74 | 75 |
{props.output}
76 |
77 | ) 78 | View.defaultProps = { a: "", b: "",output:""} 79 | ``` 80 | 81 | apply XMult to View then we'll get a React Component 82 | 83 | ```js 84 | let Mult = XMult.apply(View) 85 | ``` 86 | 87 | 88 | 89 | 90 | 91 | ## Functor 92 | 93 | FantasyX also implemented Functor, so we can transform one FantasyX to another 94 | 95 | For example, from `XMult`, we could simply transform it into a `XMMP` with new computation 96 | 97 | ```js 98 | let XMMP = XMult.map((s) => ({output: s.output * s.output})) 99 | ``` 100 | 101 | it's just like mapping on a list 102 | 103 | ```js 104 | [1,2,3].map(x=>x*x) 105 | // [2,4,6] 106 | ``` 107 | 108 | 109 | 110 | 111 | 112 | ## Monoid 113 | 114 | It's actually Semigroup, but if we have a ID FantasyX, we have Monoid, an Identity FantasyX make sense in that the computation inside is just Identity. 115 | 116 | Anyway, let's see how can we combine two FantasyX together 117 | 118 | ```js 119 | let XCOMBINE = XMMP.concat(XMult) 120 | ``` 121 | 122 | 仅此, 我们就得到了一个同时具有 XMMP 与 XMult 行为的 FantasyX 123 | 124 | 当然, 因为他们都会修改 `state.output`, 合并到一起会导致冲突, 我们稍微修改下 XMMP 125 | 126 | ```js 127 | let XMMP = XMult.map((s) => ({output2: s.output * s.output})) 128 | ``` 129 | 130 | nothing special just like how you concat two Arrays 131 | 132 | 133 | 134 | 135 | 136 | ------- 137 | 138 | Check out a more complicated BMI Calculator: 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/src/main/tut/Get-Started.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Quick Start 4 | section: en 5 | position: 1 6 | --- 7 | 8 | ![](https://www.evernote.com/l/ABcSUEkq5_xPTrWy_YdF5iM1Fxu14WMB7eAB/image.png) 9 | 10 | ## Terminology 11 | - **Machine**: a machine can emit `Update` to a timeline `update$`, and can be operated by calling function in `actions` 12 | - **Plan**: a Plan is a function that describe how to create a `Machine` 13 | - **Update**: a function of `currentState -> nextState` 14 | - **Action**: a function that create instance of `Intent` 15 | - **Intent**: describe what you want to do 16 | - **Intent Stream**: a timeline of every `Intent` created by each `Action` 17 | 18 | ## Quick Start 19 | 20 | Sorry we don't have a **book** to document how to use xreact, and I don't really need to, 21 | there's only 3 things you should notice when using xreact, I'll explain by a simple counter app. 22 | 23 |

See the Pen XREACT by Jichao Ouyang (@jcouyang) on CodePen.

24 | 25 | 26 | ### 1. Define a simple stateless View component 27 | 28 | ![](https://www.evernote.com/l/ABd-YTQc2FVBjqOEkpiFZDltPloti8a2Hq8B/image.png) 29 | 30 | ```html 31 | const CounterView = ({actions, count}) => ( 32 |
33 | 34 | {count} 35 | 36 |
37 | ) 38 | ``` 39 | 40 | every View component expected a `actions` fields in `props` 41 | 42 | ### 2. Define a `Plan` 43 | 44 | ![](https://www.evernote.com/l/ABeLlbr3vQNM_JKfcd_W4zfW262lxWJhOsMB/image.png) 45 | 46 | After we have a pretty view for representation and interacting interface, we can define how to update the view, or "how to react on actions". In such case: 47 | 48 | 1. A counter can have actions of `inc` and `dec`, which will send `Intent` of `{type: 'inc'}` or `{type:'dec'}` to `Intent Stream` upon being called. 49 | 2. A counter reactivity generates `Update` when it receives an `Intent` of either type `inc` or `dec`. 50 | 51 | ```js 52 | const plan = (intent$) => { 53 | return { 54 | update$: intent$.map(intent => { 55 | switch (intent.type) { 56 | case 'inc': 57 | return state => ({ count: state.count + 1 }); 58 | case 'dec': 59 | return state => ({ count: state.count - 1 }); 60 | default: 61 | return _ => _; 62 | } 63 | }), 64 | actions: { 65 | inc: () => ({ type: 'inc' }), 66 | dec: () => ({ type: 'dec' }) 67 | } 68 | } 69 | } 70 | ``` 71 | a `Plan` will take `intent$`(Intent Stream) and return a `Machine`. 72 | 73 | a `Machine` defines 74 | 75 | - how you can act on the machine 76 | - how the machine will react on intents. 77 | 78 | ### 3. Plan X View 79 | 80 | ![](https://www.evernote.com/l/ABdv2Ks5f7dNQKxyoz7Q1eB9Xm9vy3U11ZMB/image.png) 81 | 82 | ```js 83 | import {render} from 'react-dom' 84 | import {x, X} from 'xreact/lib/x' 85 | import * as rx from 'xreact/lib/xs/rx' 86 | 87 | const Counter = x(plan)(CounterView) 88 | 89 | render( 90 | 91 | 92 | , 93 | document.getElementById('app') 94 | ); 95 | ``` 96 | `Counter` is product(x) of `plan` and `CounterView`, which means it can react to `Intent` as it's plan, and update `CounterView` 97 | 98 | `` will provide a `intent$` instance. 99 | 100 | 101 | ## Type Safe Counter 102 | 103 | If you are TypeScript user and want to enjoy a type safe counter app, it's simple to do so since xReact is written 100% in TypeScript. 104 | 105 |

See the Pen XREACT Counter in TypeScript by Jichao Ouyang (@jcouyang) on CodePen.

106 | 107 | 108 | ## Fantasy 🌈 Counter 109 | 110 | If you come from 🌈 fantasy land, you cound try fantasy version of xreact in less verbose and more functional scheme. [More about FantasyX](./Fantasy.html) 111 | 112 |

See the Pen XREACT FANTASY COUNTER by Jichao Ouyang (@jcouyang) on CodePen.

113 | 114 | -------------------------------------------------------------------------------- /docs/src/main/tut/Home.md: -------------------------------------------------------------------------------- 1 | Welcome to the react-most wiki! 2 | # 🇬🇧 3 | ## [[FRP Best Practice]] 4 | 5 | ## [[Performance]] 6 | 7 | # 🇨🇳 8 | ## [[教程]] 9 | ## [[React Most 函数式最佳实践]] -------------------------------------------------------------------------------- /docs/src/main/tut/Monadic.md: -------------------------------------------------------------------------------- 1 | todo -------------------------------------------------------------------------------- /docs/src/main/tut/Notation.md: -------------------------------------------------------------------------------- 1 | We use the same notation as most.js 2 | 3 | - `-` - an instant in time where no event occurs 4 | - `letters (a,b,c,d,etc)` - an event at an instant in time 5 | - `|` - end of stream 6 | - `X` - an error occurred at an instant in time 7 | - `>` - stream continues infinitely 8 | 9 | [some examples](https://github.com/cujojs/most/blob/master/docs/api.md#examples) 10 | -------------------------------------------------------------------------------- /docs/src/main/tut/Performance.md: -------------------------------------------------------------------------------- 1 | `react-most` no more than creating stream from your actions, and bind it to state stream. no any other computations happen in `react-most`. so please refer to [most.js's perf](https://github.com/cujojs/most/tree/master/test/perf) which is realy Great! 2 | 3 | I also do a simple benchmark with 8k times of performing counter increase action 4 | ``` 5 | Memory Usage Before: { rss: 32501760, heapTotal: 16486912, heapUsed: 11307128 } 6 | Memory Usage After: { rss: 34418688, heapTotal: 18550784, heapUsed: 11932336 } 7 | Elapsed 8ms 8 | ``` 9 | basically the same level of performance as redux(which is 10ms in the same testing) 10 | -------------------------------------------------------------------------------- /docs/src/main/tut/React-Most-函数式最佳实践.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: xReact 函数式最佳实践 4 | section: zh 5 | --- 6 | 7 | 我将继续使用 Counter 这个简单的例子,逐渐重构以展示如何使用函数式feature 8 | 9 | 处基 10 | ==== 11 | 12 | 先来初级的函数式重构 13 | 14 | JavaScript Union Types 15 | --------------- 16 | 17 | ### 使用 Union Type 定义 `Intent` 18 | 19 | [union-type](https://github.com/paldepind/union-type) 是一个简单的提供 union type,或者说 case class 的库。 20 | 21 | 你可能见某dux框架使用的action都带有 `type` 字段,然后用 string 来区分不同的 action 这种难看不健壮的方式。 22 | 23 | ``` javascript 24 | inc: () => ({type: 'inc'}) 25 | ``` 26 | 27 | union-type 太适合解决这个问题了: 28 | 29 | `Intent.js` 30 | 31 | ``` javascript 32 | import Type from 'union-type' 33 | export default Type({ 34 | Inc: [] 35 | Dec: [] 36 | }) 37 | ``` 38 | 39 | ### case Intent, 别 switch 40 | 41 | case union-type 是 pattern matching, 不是 switch case 42 | 43 | ``` javascript 44 | import Intent from 'intent' 45 | const counterable = x(intent$ => { 46 | return { 47 | update$: intent$.map(Intent.case({ 48 | Inc: () => state => ({count: state.count + 1}), 49 | Dec: () => state => ({count: state.count - 1}), 50 | _: () => state => state 51 | })), 52 | actions: { 53 | inc: Intent.Inc, 54 | dec: Intent.Dec, 55 | } 56 | } 57 | }) 58 | ``` 59 | 60 | ### pattern match union type 61 | 62 | union type 还可以带上值,比如 `Inc` 的内容是 `Number` 63 | 64 | ``` javascript 65 | import Type from 'union-type' 66 | export default Type({ 67 | Inc: [Number] 68 | Dec: [Number] 69 | }) 70 | ``` 71 | 72 | 你可以 case `Number` 出啦 73 | 74 | ``` javascript 75 | import Intent from 'intent' 76 | const counterable = x(intent$ => { 77 | return { 78 | update$: intent$.map(Intent.case({ 79 | Inc: (value) => state => ({count: state.count + value}), 80 | Dec: (value) => state => ({count: state.count - value}), 81 | _: () => state => state 82 | })), 83 | actions: { 84 | inc: Intent.Inc, 85 | dec: Intent.Dec, 86 | } 87 | } 88 | }) 89 | ``` 90 | 91 | TypeScript Union Types 92 | ---------------------- 93 | 使用 TypeScript 的 Union Type 会简单得多, 而且运行时是zero cost, 只会在编译时检查. 94 | 95 | ```ts 96 | export interface Inc { 97 | kind: 'inc' 98 | } 99 | export interface Dec { 100 | kind: 'dec' 101 | } 102 | export type Intent = Inc | Dec 103 | ``` 104 | 105 | 稍微缺陷的是 TypeScript 没有 pattern matching 106 | 107 | ```ts 108 | const counterable = x<"Observable", Intent, State>(intent$ => { 109 | return { 110 | update$: intent$.map(intent =>{ 111 | switch(intent.kind) { 112 | case 'inc': return state => ({count: state.count + 1}) 113 | case 'dec': return state => ({count: state.count + 1}) 114 | default: return state => state' 115 | } 116 | }), 117 | actions: { 118 | inc: () => ({kind: 'inc'} as Inc), 119 | dec () => ({kind: 'dec'} as Dec) 120 | } 121 | } 122 | }) 123 | ``` 124 | 125 | 但是没关系, 编译器会保证你case的类型正确, 比如你在case 里写错类型的字符串如 `incblahblah`, 编译不会通过. 126 | 127 | lens 128 | ---- 129 | 130 | lens 是 composable, immutable, functional 的更新,观察数据结构的方式 131 | 132 | 下面是使用 [ramda](http://ramdajs.com/) 的 lens 实现的例子 133 | 134 | ``` javascript 135 | import {lens, over, inc, dec, identity} from 'ramda' 136 | const counterable = x(intent$ => { 137 | let lensCount = lens(prop('count')) 138 | return { 139 | update$: intent$.map(Intent.case({ 140 | Inc: () => over(lensCount, inc) 141 | Dec: () => over(lensCount, dec), 142 | _: () => identity 143 | })) 144 | } 145 | }) 146 | ``` 147 | 148 | flatMap 149 | ------- 150 | 151 | 当遇到异步的时候,可以简单的 flatMap 到 sink 上 152 | 153 | ``` javascript 154 | import when from 'when' 155 | import {just, from, lens, over, set, inc, dec, identity, compose} from 'ramda' 156 | const counterable = x(intent$ => { 157 | let lensCount = lens(prop('count')) 158 | return { 159 | update$: intent$.map(Intent.case({ 160 | Inc: () => over(lensCount, inc) 161 | Dec: () => over(lensCount, dec), 162 | _: () => identity 163 | })) 164 | data$: just(0) 165 | .flatMap(compose(from, when)) // <-- when is a async value 166 | .map(set(lensCount)) 167 | } 168 | }) 169 | ``` 170 | 171 | 组合 172 | ---- 173 | 174 | wrappers 是 composable, 跟函数一样 175 | 176 | ``` javascript 177 | import Type from 'union-type' 178 | export default Type({ 179 | Inc: [Number], 180 | Dec: [Number], 181 | Double: [], 182 | Half: [] 183 | }) 184 | ``` 185 | 186 | 比如还可以创建一个wrapper,可以翻倍、减半 187 | 188 | ``` javascript 189 | const doublable = x(intent$ => { 190 | let lensCount = lens(prop('count')) 191 | return { 192 | update$: intent$.map(Intent.case({ 193 | Double: () => over(lensCount, x=>x*2) 194 | Half: () => over(lensCount, x=>X/2), 195 | _: () => identity, 196 | })) 197 | actions: { 198 | double: Intent.Double, 199 | half: Intent.Half, 200 | } 201 | } 202 | }) 203 | ``` 204 | 205 | 包在 View 外面 206 | 207 | ``` javascript 208 | const Counter = doublable(increasable(CounterView)) 209 | ``` 210 | 211 | `CounterView` 就有了 `+1` `-1` `*1` `/1` 212 | 213 | ``` javascript 214 | const CounterView = props => ( 215 |
216 | 217 | 218 | {props.count} 219 | 220 | 221 |
222 | ) 223 | ``` 224 | 225 | 现在我们的Counter 就变成了[这样](https://github.com/reactive-react/react-most/blob/master/examples/frp-counter/src/app.jsx) 226 | 227 | 228 | 搞基 229 | ==== 230 | 231 | 掌握了 lens,union-type, flatmap, compose 的概念之后,如果还不够爽,可以用一些更搞基的pattern来让代码的 ~~逼格~~ 扩展性更高一些。比如: 232 | 233 | [FantasyX](https://xreact.oyanglul.us/%E8%8C%83%E7%89%B9%E8%A5%BF.html) 234 | ------------------------------------------------------------- 235 | Fantasy land 标准的 Functor, Monoid, Applicative 236 | 237 | [Data types à la carte](https://github.com/jcouyang/alacarte) 238 | ------------------------------------------------------------- 239 | 240 | 简单的说还是interpreter pattern,但不是用 free monad, 是更简单的combinator,瞬间就能去掉pattern match 和action定义的表达式扩展问题, [比如](https://github.com/jcouyang/alacarte/wiki/读我) 241 | -------------------------------------------------------------------------------- /docs/src/main/tut/_Footer.md: -------------------------------------------------------------------------------- 1 | [have a question? join the ![](https://badges.gitter.im/jcouyang/react-most.svg)](https://gitter.im/jcouyang/react-most?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -------------------------------------------------------------------------------- /docs/src/main/tut/_Sidebar.md: -------------------------------------------------------------------------------- 1 | ## :gb: 2 | ### [[FRP Best Practice]] 3 | 4 | ### [[Performance]] 5 | 6 | ### [[Roadmap]] 7 | 8 | ## :cn: 9 | ### [[教程]] 10 | ### [[React Most 函数式最佳实践]] -------------------------------------------------------------------------------- /docs/src/main/tut/circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | only: 4 | - master 5 | -------------------------------------------------------------------------------- /docs/src/main/tut/examples/example.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import '../../../../../src/xs/rx' 4 | import { Applicative, lift2,Semigroup, Functor, map, Traversable, FlatMap } from '../../../../../src/fantasy' 5 | import {X} from '../../../../../src' 6 | function xmount(component, dom) { render(React.createFactory(X)({}, component), dom) } 7 | 8 | let mult = (x:number,y: number) => x * y 9 | let Xeg1 = lift2<"FantasyX",number, number, number>(mult)(Applicative.FantasyX.pure(6), Applicative.FantasyX.pure(5)) 10 | 11 | let ViewEg1 = props =>

{props.product}

12 | 13 | let Eg1 = Functor.FantasyX.map(a=>({product: a}), Xeg1).apply(ViewEg1) 14 | 15 | xmount(, document.getElementById('eg1') ) 16 | 17 | import {Xstream} from '../../../../../src/fantasy/xstream'; 18 | 19 | function strToInt(x) {return ~~x} 20 | type Intent = {type:string, value:number} 21 | let XSinput1 = Xstream.fromEvent('change', 'n1', '5') 22 | let XSinput2 = Xstream.fromEvent('change', 'n2', '6') 23 | 24 | let Xeg2 = lift2<"Xstream", number, number, number>(mult)( 25 | Functor.Xstream.map(strToInt, XSinput1), 26 | Functor.Xstream.map(strToInt, XSinput2) 27 | ).toFantasyX() 28 | .map(x=>({product: x})) 29 | 30 | let ViewEg2 = props =>
31 |

32 |

33 |

{props.product}

34 |
35 | 36 | let Eg2 = Xeg2.apply(ViewEg2) 37 | 38 | xmount(, document.getElementById('eg2') ) 39 | 40 | let Xeg3 = Semigroup.Xstream.concat( 41 | Semigroup.Xstream.concat( 42 | Xstream.fromEvent('change', 'firstName', 'Jichao'), 43 | Applicative.Xstream.pure(' ') 44 | ), 45 | Xstream.fromEvent('change', 'lastName', 'Ouyang') 46 | ).toFantasyX() 47 | let ViewEg3 = props =>
48 |

49 |

50 |

{props.semigroup}

51 |
52 | 53 | let Eg3 = Xeg3.map(a=>({semigroup: a})).apply(ViewEg3) 54 | 55 | xmount(, document.getElementById('eg3') ) 56 | 57 | function sum(list){ 58 | return list.reduce((acc,x)=> acc+x, 0) 59 | } 60 | let list = ['1', '2', '3', '4', '5', '6', '7'] 61 | let Xeg4 = Traversable.Array.traverse<'Xstream', string, string>('Xstream')( 62 | (defaultVal, index) => (Xstream.fromEvent('change', 'traverse' + index, defaultVal)), 63 | list 64 | ).toFantasyX() 65 | .map(xs => xs.map(strToInt)) 66 | .map(sum) 67 | 68 | let ViewEg4 = props =>
69 | {list.map((item, index) => (

70 | 71 |

)) 72 | } 73 |

{props.sum}

74 |
75 | 76 | let Eg4 = Xeg4.map(a=>({sum: a})).apply(ViewEg4) 77 | 78 | xmount(, document.getElementById('eg4') ) 79 | 80 | function bmiCalc(weight, height) { 81 | return fetch(`https://gist.github.com.ru/jcouyang/edc3d175769e893b39e6c5be12a8526f?height=${height}&weight=${weight}`) 82 | .then(resp => resp.json()) 83 | .then(json => json.result) 84 | } 85 | 86 | let xweigth = Xstream.fromEvent('change', 'weight', '70') 87 | let xheight = Xstream.fromEvent('change', 'height', '175') 88 | 89 | let promiseXstream = lift2<"Xstream", string, string, Promise>(bmiCalc)( 90 | xweigth, 91 | xheight 92 | ) 93 | 94 | let Xeg5 = FlatMap.Xstream.flatMap(Xstream.fromPromise, promiseXstream) 95 | .toFantasyX() 96 | 97 | let ViewEg5 = props => ( 98 |
99 | 102 | 105 |

HEALTH: {props.health}

106 |

BMI: {props.bmi}

107 |
108 | ) 109 | 110 | let Eg5 = Xeg5.apply(ViewEg5) 111 | 112 | xmount(, document.getElementById('eg5') ) 113 | 114 | let Xeg6 = Xstream.fromEvent('click', 'increment') 115 | .toFantasyX<{count:number}>() 116 | .map(x => 1) 117 | .foldS((acc, a) => { 118 | return { count: (acc.count||0) + a }}) 119 | 120 | let ViewEg6 = props =>

121 | {props.count || 0} 122 | props.actions.fromEvent(e)} /> 123 |

124 | 125 | let Eg6 = Xeg6.apply(ViewEg6) 126 | 127 | xmount(, document.getElementById('eg6') ) 128 | 129 | let Xeg7 = Xstream.fromEvent('click', 'decrement') 130 | .toFantasyX<{count:number}>() 131 | .map(x => -1) 132 | .foldS((acc, a) => { 133 | return { count: (acc.count||0) + a }}) 134 | 135 | let ViewEg7 = props =>

136 | props.actions.fromEvent(e)} /> 137 | {props.count || 0} 138 | props.actions.fromEvent(e)} /> 139 |

140 | 141 | let Eg7 = Xeg7.merge(Xeg6).apply(ViewEg7) 142 | 143 | xmount(, document.getElementById('eg7') ) 144 | 145 | const actions = ['-1', '+1', 'reset'] 146 | let Xeg8 = 147 | actions.map((action)=>Xstream.fromEvent('click', action).toFantasyX<{count:number}>()) 148 | .reduce((acc,a)=>acc.merge(a)) 149 | .foldS((acc, i) => { 150 | acc.count = acc.count || 0 151 | switch(i) { 152 | case '-1': return {count: acc.count -1} 153 | case '+1': return {count: acc.count +1} 154 | case 'reset': return {count: 0} 155 | default: acc 156 | } 157 | } 158 | ) 159 | 160 | let ViewEg8 = props =>

161 | {props.count} 162 | {actions.map(action=> 163 | props.actions.fromEvent(e)} />)} 164 |

165 | 166 | let Eg8 = Xeg8.apply(ViewEg8) 167 | 168 | xmount(, document.getElementById('eg8') ) 169 | -------------------------------------------------------------------------------- /docs/src/main/tut/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Examples 4 | section: en 5 | position: 6 6 | --- 7 | 8 | - [import xreact](#org202661d) 9 | - [Example 1: Two number multiply](#orgd564fbd) 10 | - [Example 2: Two Inputs](#org5677d81) 11 | - [Example 3: Semigroup](#org863e564) 12 | - [Example 4: Traverse](#orgfd54baa) 13 | - [Example 5: Asynchronous](#orgd82b260) 14 | - [Example 6: Fold](#org7a7a3b6) 15 | - [Example 7: Merge](#orga97410a) 16 | - [Example 8: Fold multiple buttons](#orgdd7473f) 17 | - [Example 13: Adding items to a list](#orgb3b8b4e) 18 | 19 | 20 | 21 | 22 | 23 | # import xreact 24 | 25 | ```tsx 26 | import {pure, lift2, X, xinput, fromEvent, traverse, fold} from '../src' 27 | import * as React from 'react'; 28 | import { render } from 'react-dom'; 29 | import * as RX from '../src/xs/rx' 30 | function xmount(component, dom) {render(React.createFactory(X)({ x: RX }, component), dom)} 31 | ``` 32 | 33 | 34 | 35 | 36 | # Example 1: Two number multiply 37 | 38 | ```tsx 39 | let Xeg1 = lift2((x:number,y: number) => x * y)(pure(6), pure(5)) 40 | let ViewEg1 = props =>

{props.product}

41 | let Eg1 = Xeg1.map(a=>({product: a})).apply(ViewEg1) 42 | ``` 43 | 44 |

45 | 46 | 47 | 48 | 49 | # Example 2: Two Inputs 50 | 51 | ```tsx 52 | let Xeg2 = lift2((x:number,y: number) => x * y)(fromEvent('change', 'n1', '5').map(x=>~~x), fromEvent('change', 'n2', '6').map(x=>~~x)) 53 | let ViewEg2 = props =>
54 |

55 |

56 |

{props.product}

57 |
58 | let Eg2 = Xeg2.map(a=>({product: a})).apply(ViewEg2) 59 | ``` 60 | 61 |

62 | 63 | 64 | 65 | 66 | # Example 3: Semigroup 67 | 68 | ```tsx 69 | let Xeg3 = fromEvent('change', 'string1', 'Jichao') 70 | .concat(pure(' ')) 71 | .concat(fromEvent('change', 'string2', 'Ouyang')) 72 | 73 | let ViewEg3 = props =>
74 |

75 |

76 |

{props.semigroup}

77 |
78 | 79 | let Eg3 = Xeg3.map(a=>({semigroup: a})).apply(ViewEg3) 80 | ``` 81 | 82 |

83 | 84 | 85 | 86 | 87 | # Example 4: Traverse 88 | 89 | ```tsx 90 | function sum(list){ 91 | return list.reduce((acc,x)=> acc+x, 0) 92 | } 93 | let list = ['1', '2', '3', '4', '5', '6', '7'] 94 | let Xeg4 = traverse( 95 | (defaultVal, index)=>(fromEvent('change', 'traverse'+index, defaultVal)), 96 | list) 97 | .map(xs=>xs.map(x=>~~x)) 98 | .map(sum) 99 | 100 | let ViewEg4 = props =>
101 | {list.map((item, index) => (

102 | 103 |

)) 104 | } 105 |

{props.sum}

106 |
107 | 108 | let Eg4 = Xeg4.map(a=>({sum: a})).apply(ViewEg4) 109 | ``` 110 | 111 |

112 | 113 | 114 | 115 | 116 | # Example 5: Asynchronous 117 | 118 | ```tsx 119 | function bmiCalc(weight, height) { 120 | return { 121 | result:fetch(`https://gist.github.com.ru/jcouyang/edc3d175769e893b39e6c5be12a8526f?height=${height}&weight=${weight}`) 122 | .then(resp => resp.json()) 123 | .then(resp => resp.result) 124 | } 125 | } 126 | 127 | let Xeg5 = lift2(bmiCalc)(fromEvent('change', 'weight', '70'), fromEvent('change', 'height', '175')) 128 | 129 | let ViewEg5 = props => ( 130 |
131 | 134 | 137 |

HEALTH: {props.health}

138 |

BMI: {props.bmi}

139 |
140 | ) 141 | 142 | let Eg5 = Xeg5.apply(ViewEg5) 143 | ``` 144 | 145 |

146 | 147 | 148 | 149 | 150 | # Example 6: Fold 151 | 152 | ```tsx 153 | let Xeg6 = fold((acc:number,i: number) => acc+i, 0, fromEvent('click', 'increment').map(x=>1)) 154 | let ViewEg6 = props =>

155 | {props.count} 156 | props.actions.fromEvent(e)} /> 157 |

158 | let Eg6 = Xeg6.map(a=>({count: a})).apply(ViewEg6) 159 | ``` 160 | 161 |

162 | 163 | 164 | 165 | 166 | # Example 7: Merge 167 | 168 | ```tsx 169 | let Xeg7 = fold( 170 | (acc:number,i: number) => acc+i, 0, 171 | fromEvent('click', 'increment').map(x=>1).merge( 172 | fromEvent('click', 'decrement').map(x=>-1))) 173 | 174 | let ViewEg7 = props =>

175 | props.actions.fromEvent(e)} /> 176 | {props.count} 177 | props.actions.fromEvent(e)} /> 178 |

179 | let Eg7 = Xeg7.map(a=>({count: a})).apply(ViewEg7) 180 | ``` 181 | 182 |

183 | 184 | 185 | 186 | 187 | # Example 8: Fold multiple buttons 188 | 189 |

190 | 191 | 192 | 193 | 194 | 195 | 196 | # Example 13: Adding items to a list 197 | -------------------------------------------------------------------------------- /docs/src/main/tut/examples/index.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Examples of xReact Fantasy 2 | #+Date: <2017-09-09 Sat> 3 | # #+AUTHOR: 欧阳继超 4 | #+HTML_HEAD: 5 | #+OPTIONS: exports:source tangle:yes eval:no-export num:1 6 | 7 | * Example 0: How to use xReact Fantasy 8 | 9 | #+BEGIN_SRC js :tangle example.tsx 10 | 11 | import * as React from 'react'; 12 | import { render } from 'react-dom'; 13 | import '../../../../../src/xs/rx' 14 | import { Applicative, lift2,Semigroup, Functor, map, Traversable, FlatMap } from '../../../../../src/fantasy' 15 | import {X} from '../../../../../src' 16 | function xmount(component, dom) { render(React.createFactory(X)({}, component), dom) } 17 | #+END_SRC 18 | 19 | * Example 1: Two number multiply 20 | A multiply function can be simply lifted to be a function that works with xReact FantasyX 21 | #+BEGIN_SRC js :tangle example.tsx 22 | let mult = (x:number,y: number) => x * y 23 | let Xeg1 = lift2<"FantasyX",number, number, number>(mult)(Applicative.FantasyX.pure(6), Applicative.FantasyX.pure(5)) 24 | #+END_SRC 25 | 26 | A very basic Stateless Component to display it. 27 | #+BEGIN_SRC js :tangle example.tsx 28 | let ViewEg1 = props =>

{props.product}

29 | #+END_SRC 30 | 31 | #+BEGIN_SRC js :tangle example.tsx 32 | let Eg1 = Functor.FantasyX.map(a=>({product: a}), Xeg1).apply(ViewEg1) 33 | #+END_SRC 34 | 35 | #+BEGIN_SRC js :tangle example.tsx 36 | xmount(, document.getElementById('eg1') ) 37 | #+END_SRC 38 | 39 | #+HTML:

40 | 41 | 42 | * Example 2: Two Inputs 43 | #+BEGIN_SRC js :tangle example.tsx 44 | import {Xstream} from '../../../../../src/fantasy/xstream'; 45 | 46 | function strToInt(x) {return ~~x} 47 | type Intent = {type:string, value:number} 48 | let XSinput1 = Xstream.fromEvent('change', 'n1', '5') 49 | let XSinput2 = Xstream.fromEvent('change', 'n2', '6') 50 | 51 | let Xeg2 = lift2<"Xstream", number, number, number>(mult)( 52 | Functor.Xstream.map(strToInt, XSinput1), 53 | Functor.Xstream.map(strToInt, XSinput2) 54 | ).toFantasyX() 55 | .map(x=>({product: x})) 56 | 57 | let ViewEg2 = props =>
58 |

59 |

60 |

{props.product}

61 |
62 | 63 | let Eg2 = Xeg2.apply(ViewEg2) 64 | #+END_SRC 65 | 66 | #+BEGIN_SRC js :tangle example.tsx :exports none 67 | xmount(, document.getElementById('eg2') ) 68 | #+END_SRC 69 | 70 | #+HTML:

71 | 72 | * Example 3: Semigroup 73 | #+BEGIN_SRC js :tangle example.tsx 74 | let Xeg3 = Semigroup.Xstream.concat( 75 | Semigroup.Xstream.concat( 76 | Xstream.fromEvent('change', 'firstName', 'Jichao'), 77 | Applicative.Xstream.pure(' ') 78 | ), 79 | Xstream.fromEvent('change', 'lastName', 'Ouyang') 80 | ).toFantasyX() 81 | let ViewEg3 = props =>
82 |

83 |

84 |

{props.semigroup}

85 |
86 | 87 | let Eg3 = Xeg3.map(a=>({semigroup: a})).apply(ViewEg3) 88 | #+END_SRC 89 | 90 | #+BEGIN_SRC js :tangle example.tsx :exports none 91 | xmount(, document.getElementById('eg3') ) 92 | #+END_SRC 93 | 94 | #+HTML:

95 | 96 | * Example 4: Traverse 97 | 98 | #+BEGIN_SRC js :tangle example.tsx 99 | function sum(list){ 100 | return list.reduce((acc,x)=> acc+x, 0) 101 | } 102 | let list = ['1', '2', '3', '4', '5', '6', '7'] 103 | let Xeg4 = Traversable.Array.traverse<'Xstream', string, string>('Xstream')( 104 | (defaultVal, index) => (Xstream.fromEvent('change', 'traverse' + index, defaultVal)), 105 | list 106 | ).toFantasyX() 107 | .map(xs => xs.map(strToInt)) 108 | .map(sum) 109 | 110 | let ViewEg4 = props =>
111 | {list.map((item, index) => (

112 | 113 |

)) 114 | } 115 |

{props.sum}

116 |
117 | 118 | let Eg4 = Xeg4.map(a=>({sum: a})).apply(ViewEg4) 119 | #+END_SRC 120 | 121 | #+BEGIN_SRC js :tangle example.tsx :exports none 122 | xmount(, document.getElementById('eg4') ) 123 | #+END_SRC 124 | 125 | #+HTML:

126 | 127 | 128 | * Example 5: Asynchronous 129 | 130 | #+BEGIN_SRC js :tangle example.tsx 131 | function bmiCalc(weight, height) { 132 | return fetch(`https://gist.github.com.ru/jcouyang/edc3d175769e893b39e6c5be12a8526f?height=${height}&weight=${weight}`) 133 | .then(resp => resp.json()) 134 | .then(json => json.result) 135 | } 136 | 137 | let xweigth = Xstream.fromEvent('change', 'weight', '70') 138 | let xheight = Xstream.fromEvent('change', 'height', '175') 139 | 140 | let promiseXstream = lift2<"Xstream", string, string, Promise>(bmiCalc)( 141 | xweigth, 142 | xheight 143 | ) 144 | 145 | let Xeg5 = FlatMap.Xstream.flatMap(Xstream.fromPromise, promiseXstream) 146 | .toFantasyX() 147 | 148 | let ViewEg5 = props => ( 149 |
150 | 153 | 156 |

HEALTH: {props.health}

157 |

BMI: {props.bmi}

158 |
159 | ) 160 | 161 | let Eg5 = Xeg5.apply(ViewEg5) 162 | #+END_SRC 163 | 164 | #+BEGIN_SRC js :tangle example.tsx :exports none 165 | xmount(, document.getElementById('eg5') ) 166 | #+END_SRC 167 | 168 | #+HTML:

169 | 170 | * Example 6: Fold 171 | 172 | #+BEGIN_SRC js :tangle example.tsx 173 | let Xeg6 = Xstream.fromEvent('click', 'increment') 174 | .toFantasyX<{count:number}>() 175 | .map(x => 1) 176 | .foldS((acc, a) => { 177 | return { count: (acc.count||0) + a }}) 178 | 179 | let ViewEg6 = props =>

180 | {props.count || 0} 181 | props.actions.fromEvent(e)} /> 182 |

183 | 184 | let Eg6 = Xeg6.apply(ViewEg6) 185 | #+END_SRC 186 | 187 | #+BEGIN_SRC js :tangle example.tsx :exports none 188 | xmount(, document.getElementById('eg6') ) 189 | #+END_SRC 190 | 191 | #+HTML:

192 | 193 | * Example 7: Merge 194 | #+BEGIN_SRC js :tangle example.tsx 195 | let Xeg7 = Xstream.fromEvent('click', 'decrement') 196 | .toFantasyX<{count:number}>() 197 | .map(x => -1) 198 | .foldS((acc, a) => { 199 | return { count: (acc.count||0) + a }}) 200 | 201 | let ViewEg7 = props =>

202 | props.actions.fromEvent(e)} /> 203 | {props.count || 0} 204 | props.actions.fromEvent(e)} /> 205 |

206 | 207 | let Eg7 = Xeg7.merge(Xeg6).apply(ViewEg7) 208 | #+END_SRC 209 | 210 | #+BEGIN_SRC js :tangle example.tsx :exports none 211 | xmount(, document.getElementById('eg7') ) 212 | #+END_SRC 213 | 214 | #+HTML:

215 | 216 | 217 | * Example 8: Fold multiple buttons 218 | 219 | #+BEGIN_SRC js :tangle example.tsx 220 | const actions = ['-1', '+1', 'reset'] 221 | let Xeg8 = 222 | actions.map((action)=>Xstream.fromEvent('click', action).toFantasyX<{count:number}>()) 223 | .reduce((acc,a)=>acc.merge(a)) 224 | .foldS((acc, i) => { 225 | acc.count = acc.count || 0 226 | switch(i) { 227 | case '-1': return {count: acc.count -1} 228 | case '+1': return {count: acc.count +1} 229 | case 'reset': return {count: 0} 230 | default: acc 231 | } 232 | } 233 | ) 234 | 235 | let ViewEg8 = props =>

236 | {props.count} 237 | {actions.map(action=> 238 | props.actions.fromEvent(e)} />)} 239 |

240 | 241 | let Eg8 = Xeg8.apply(ViewEg8) 242 | 243 | xmount(, document.getElementById('eg8') ) 244 | #+END_SRC 245 | 246 | #+HTML:

247 | 248 | #+HTML: 249 | 250 | # #+HTML: 251 | -------------------------------------------------------------------------------- /docs/src/main/tut/flatMap.md: -------------------------------------------------------------------------------- 1 | flatMap is simply `flatten` compose `map` 2 | 3 | imaging when you 4 | 5 | 1. map an Array `[1,2,3]` with function `x=>[x]`, you'll get `[[1],[2],[3]]` 6 | 2. `flatten` will flatten the nested array into a flat array `[1,2,3]` 7 | 8 | same thing happen to Stream 9 | 10 | 1. map `--1--2--3-->` with function `x=>Stream(x+1)` will return `--S(1+1)--S(2+1)--S(3+1)-->` 11 | 2. so if `S(1+1)` represent as `--2-->`, `flatten` will flatten the nested Stream into flat Stream `----2----3----4-->` 12 | 13 | > ref to [[Notation]] if these symbols make no sense to you. 14 | -------------------------------------------------------------------------------- /docs/src/main/tut/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: "Reactive x React" 4 | section: "home" 5 | technologies: 6 | - first: ["Functional", "Declarative and composable data flow"] 7 | - second: ["Reactive", "Asynchronous flow made easy"] 8 | - third: ["React", "Seamless and idiomatic integration with React"] 9 | --- 10 | 11 | xReact is a lightweight reactive HOC for React. Data flow in xReact is observable and unidirectional. 12 | 13 | ![](https://www.evernote.com/l/ABdv2Ks5f7dNQKxyoz7Q1eB9Xm9vy3U11ZMB/image.png) 14 | 15 | ## Get Started 16 | ``` 17 | npm install xreact --save 18 | # or 19 | yarn add xreact 20 | ``` 21 | 22 | - Come from redux? 👉 23 | - Come from fantasy land? 🌈 24 | - Examples 👇 25 | - 26 | - 27 | 28 | ## Features 29 | 30 | ### Purely Functional, Declarative, and Monadic 31 | In imperatively code, you have to describe verbose of how to process data. With `xreact`, we simply define data transformations, then compose them to form data flow. There are no variables, no intermediate state, and no side effects in your data flow's data composition! 32 | 33 | For FP maniac, you will definitely have fun with [FantasyX](https://xreact.oyanglul.us/Fantasy.html), which implemented Functor and Applicative type class instances on top of xReact, with State Monad. 34 | 35 | ### Typesafe and scalable 36 | xReact is 100% Typescript! Turn your runtime bugs into compile time errors, fail and fix early. 37 | 38 | ### High level but 0 cost abstraction 39 | In Redux, reducers' use of `switch` statements can make them difficult to compose. Unlike reducers, the function `x` return is simply a function which can easily compose. 40 | 41 | ```js 42 | const Counter = x(plan1)(x(plan2)(CounterView)) 43 | // is the same as 44 | const plan1_x_plan2_x = compose(x(plan1), x(plan2)) 45 | const Counter = plan1_x_plan2_x(CounterView) 46 | ``` 47 | 48 | What really happen behind compose is actually ES6 style mixin, so there won't be any extra layer of HoC or any performance overhead. 49 | 50 | ### Asynchronous made easy 51 | Asynchronous functions, such as Promises, can be converted to a stream and then flat-mapped. 52 | 53 | ```js 54 | intent$.filter(x=>x.kind=='rest') 55 | .flatMap(({url}) => fromPromise(fetch(url))) 56 | ``` 57 | 58 | where `fetch(url)` will return a `Promise`, and fromPromise will transform a `Promise` into `Observable`, so you can flat it in intent stream which is also a `Observable`. 59 | 60 | ### Reactive libs of your choice 61 | xReact came with 2 FRP libs of choices, rxjs and mostjs, for any new libs you only need to implement the `StaticStream` with your preferred lib as Higher Kind Type, just like how we done for [mostjs](https://github.com/reactive-react/xreact/blob/master/src/xs/most.ts). 62 | 63 | More details about HKT implementation in TypeScript is [here](https://github.com/gcanti/fp-ts), but you don't really need to know this. 64 | 65 | ## Copyright and License 66 | All code is available to you under the MIT license. The design is informed by many other projects: 67 | - [rxjs](https://github.com/ReactiveX/rxjs) 68 | - [most](https://github.com/cujojs/most) 69 | - [React](http://facebook.github.io/react/) 70 | - [purescript-flare](https://github.com/sharkdp/purescript-flare) 71 | - [redux](https://github.com/rackt/redux) 72 | - [Om](https://github.com/omcljs/om) 73 | - [Cycle](http://cycle.js.org/) 74 | - [transdux](https://github.com/jcouyang/transdux) 75 | -------------------------------------------------------------------------------- /docs/src/main/tut/zh.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: 读我 4 | section: zh 5 | position: 2 6 | --- 7 | 8 | ![](https://www.evernote.com/l/ABcSUEkq5_xPTrWy_YdF5iM1Fxu14WMB7eAB/image.png) 9 | 10 | # 单词表 11 | 12 | - **Machine**: 有一个 `update$` 流,内部放着 `Update`类型的数据, 还有一些 `actions`,调用`actions`会将 `Intent` 放入 `intent$` 13 | - **Plan**: 是 `Machine` 的工厂方法 14 | - **Update**: 从旧 state 到新 state 的函数 `currentState -> nextState` 15 | - **Action**: 创建 `Intent` 实例并放到 `intent$` 中 16 | - **Intent**: 要做的事情 17 | - **Intent Stream**: `Intent` 实例创建的时间线 18 | 19 | # 快速开始 20 | 我将用 =counter= 作为例子来引入 xreact. 21 | 22 | 基本上使用 =xreact= 只需要3步. 23 | 24 | 1. 创建一个无状态的view component 25 | 2. 创建一个定义machine 行为的 plan 26 | 3. plan x view 27 | 28 | ## 1. 创建一个无状态的 view component 29 | ![](https://www.evernote.com/l/ABd-YTQc2FVBjqOEkpiFZDltPloti8a2Hq8B/image.png) 30 | 31 | ```html 32 | const CounterView = ({actions, count}) => ( 33 |
34 | 35 | {count} 36 | 37 |
38 | ) 39 | ``` 40 | 41 | View 期待 props.actions 会有一些操作,想象成把机器人 machine 的遥控器传给了 View 42 | 43 | ## 2. 制定如何制造Machine的 Plan 44 | 45 | ![](https://www.evernote.com/l/ABeLlbr3vQNM_JKfcd_W4zfW262lxWJhOsMB/image.png) 46 | 47 | 计划机器人会响应事件流 `intent$` 中的两种 `Intent`,而且遥控器actions可以有 `inc` 和 `dec` 两种操作 48 | 49 | ```js 50 | const plan = (intent$) => { 51 | return { 52 | update$: intent$.map(intent => { 53 | switch (intent.type) { 54 | case 'inc': 55 | return state => ({ count: state.count + 1 }); 56 | case 'dec': 57 | return state => ({ count: state.count - 1 }); 58 | default: 59 | return _ => _; 60 | } 61 | }), 62 | actions: { 63 | inc: () => ({ type: 'inc' }), 64 | dec: () => ({ type: 'dec' }) 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | ## 3. plan x view 71 | 下来,把 plan 和 view 乘一起就好了 72 | 73 | ![](https://www.evernote.com/l/ABdv2Ks5f7dNQKxyoz7Q1eB9Xm9vy3U11ZMB/image.png) 74 | 75 | ```js 76 | import {render} from 'react-dom' 77 | import X, {x} from 'xreact/lib/x' 78 | import * as rx from 'xreact/lib/xs/rx' 79 | 80 | const Counter = x(plan)(CounterView) 81 | 82 | render( 83 | 84 | 85 | , 86 | document.getElementById('app') 87 | ); 88 | ``` 89 | `Counter` 是 `plan` 和 `CounterView` 的乘积, 意思是 Counter 现在有一个机器人,它连上了一个触摸的显示屏,可以点,可以显示状态 90 | 91 | `` 会给 Counter 提供 `intent$` 实例. 92 | 93 | 94 | ## 接下来 95 | 96 | - [[React-Most 函数式最佳实践]] 97 | -------------------------------------------------------------------------------- /docs/src/main/tut/教程.md: -------------------------------------------------------------------------------- 1 | ![](https://www.evernote.com/l/ABcSUEkq5_xPTrWy_YdF5iM1Fxu14WMB7eAB/image.png) 2 | 3 | # 单词表 4 | 5 | - **Machine**: 有一个 `update$` 流,内部放着 `Update`类型的数据, 还有一些 `actions`,调用`actions`会将 `Intent` 放入 `intent$` 6 | - **Plan**: 是 `Machine` 的工厂方法 7 | - **Update**: 从旧 state 到新 state 的函数 `currentState -> nextState` 8 | - **Action**: 创建 `Intent` 实例并放到 `intent$` 中 9 | - **Intent**: 要做的事情 10 | - **Intent Stream**: `Intent` 实例创建的时间线 11 | 12 | # 快速开始 13 | 我将用 =counter= 作为例子来引入 xreact. 14 | 15 | 基本上使用 =xreact= 只需要3步. 16 | 17 | 1. 创建一个无状态的view component 18 | 2. 创建一个定义machine 行为的 plan 19 | 3. plan x view 20 | 21 | ## 1. 创建一个无状态的 view component 22 | ![](https://www.evernote.com/l/ABd-YTQc2FVBjqOEkpiFZDltPloti8a2Hq8B/image.png) 23 | 24 | ```html 25 | const CounterView = ({actions, count}) => ( 26 |
27 | 28 | {count} 29 | 30 |
31 | ) 32 | ``` 33 | 34 | View 期待 props.actions 会有一些操作,想象成把机器人 machine 的遥控器传给了 View 35 | 36 | ## 2. 制定如何制造Machine的 Plan 37 | 38 | ![](https://www.evernote.com/l/ABeLlbr3vQNM_JKfcd_W4zfW262lxWJhOsMB/image.png) 39 | 40 | 计划机器人会响应事件流 `intent$` 中的两种 `Intent`,而且遥控器actions可以有 `inc` 和 `dec` 两种操作 41 | 42 | ```js 43 | const plan = (intent$) => { 44 | return { 45 | update$: intent$.map(intent => { 46 | switch (intent.type) { 47 | case 'inc': 48 | return state => ({ count: state.count + 1 }); 49 | case 'dec': 50 | return state => ({ count: state.count - 1 }); 51 | default: 52 | return _ => _; 53 | } 54 | }), 55 | actions: { 56 | inc: () => ({ type: 'inc' }), 57 | dec: () => ({ type: 'dec' }) 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ## 3. plan x view 64 | 下来,把 plan 和 view 乘一起就好了 65 | 66 | ![](https://www.evernote.com/l/ABdv2Ks5f7dNQKxyoz7Q1eB9Xm9vy3U11ZMB/image.png) 67 | 68 | ```js 69 | import {render} from 'react-dom' 70 | import X, {x} from 'xreact/lib/x' 71 | import * as rx from 'xreact/lib/xs/rx' 72 | 73 | const Counter = x(plan)(CounterView) 74 | 75 | render( 76 | 77 | 78 | , 79 | document.getElementById('app') 80 | ); 81 | ``` 82 | `Counter` 是 `plan` 和 `CounterView` 的乘积, 意思是 Counter 现在有一个机器人,它连上了一个触摸的显示屏,可以点,可以显示状态 83 | 84 | `` 会给 Counter 提供 `intent$` 实例. 85 | 86 | 87 | ## 接下来 88 | 89 | - [[React-Most 函数式最佳实践]] 90 | -------------------------------------------------------------------------------- /docs/src/main/tut/范特西.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: 范特西 4 | section: zh 5 | position: 4 6 | --- 7 | 8 | # xReact 函数式范特西 9 | 10 | xReact 2.3.0 后加入了一个新功能,那就是一个新的类型 `FantasyX`。这个类型,会彻底改变编写前端控件的方式。 11 | 12 | > 如果接触过 PureScript 的同学,估计已经对一个叫 [Flare](https://david-peter.de/articles/flare/)的库有所了解,`FantasyX` 正是 Flare 在 xReact 的实现,但是不同的是,xReact 并没有把 UI 和逻辑揉到一起定义。 13 | 14 | ## 只需一个例子 15 | 16 | 想象一下我们要实现一个函数来做乘法操作,如果不涉及 UI,只是输入两个数,输出结果,会是多么简单的一件事情: 17 | 18 | ```js 19 | // Number -> Number -> Number 20 | function mult(a, b) { 21 | return a * b 22 | } 23 | mult(1, 2) 24 | ``` 25 | 26 | 但是如果我们要写成 UI 控件, 将会变得多么复杂: 27 | 28 | - 两个输入控件 `` 29 | - 一个输出控件用来显示结果 30 | - 绑定函数到 input 的 onChange 上,一旦任何一个 input 发生变化, 则回调函数 31 | - 还要从函数获取两个 input 的值作为输入 32 | 33 | 天哪, 为什么前端程序员每天都要干这么蠢的事情. 就算使用 React 来做组件,这些过程也一样不能少. 34 | 35 | 所以才有了 `FantasyX`, 他帮助你构造一起 UI 组件的 Monad, Applicative, Functor, Monoid, 如果你没听过这些词, 没关系,例子会解释一切. 36 | 37 | 还是实现乘法, 非常简单,只需要把普通函数 lift 起来: 38 | 39 | ```js 40 | // FantasyX -> FantasyX -> FantasyX 41 | let xMult = lift2(mult) 42 | ``` 43 | 44 | mult 有两个参数, 所以我们需要 `lift2`, 得到的 `xMult` 依然是函数, 但是 你会发现他的类型从 接收 Number 并返回 Number 的函数,变成了接收 FantasyX 返回 FantasyX 的函数. 这种操作就叫做 `lift` 45 | 46 | 有了这个函数, 我们需要找输入了, `mult` 需要两个为 Number 的输入, 那么我们的 FantasyX 类型的输入怎么给呢? 47 | 48 | ```js 49 | let XMult = xMult(xinput('a'), xinput('b')) 50 | ``` 51 | 52 | 擦,不能这么简单吧. `xinput` 又是什么鬼? 53 | 54 | 你管,反正这里的 xinput 帮你构造了一个 FantasyX, 在里面编制着炫酷的数据流. 55 | 56 | 可能是这样的 57 | 58 | ![](https://xreact.oyanglul.us/img/futurama_June_22__2016_at_0120AM.gif) 59 | 60 | 或者是这样的 61 | 62 | ![](https://xreact.oyanglul.us/img/futurama_September_11__2016_at_0545AM (2).gif) 63 | 64 | 自己看源码吧. 不过看之前可能你会需要了解 [State Monad](https://github.com/reactive-react/xreact/blob/master/src/fantasy/state.ts) 65 | 66 | 要知道, 到此为止我们得到的 `XMult` 依然是 FantasyX 类型. 先要得到一个正常的 React Component, 只需要 apply 到一个 React Component 上. 67 | 68 | 一个简单 Stateless Component 69 | ```js 70 | const View = props => ( 71 |
72 | 73 | 74 |
{props.output}
75 |
76 | ) 77 | View.defaultProps = { a: "", b: "",output:""} 78 | ``` 79 | 80 | apply 到 View 上获得 React Component 一枚 81 | ```js 82 | let Mult = XMult.apply(View) 83 | ``` 84 | 85 | 完整代码: 86 | 87 | 88 | 89 | ## Functor 90 | 91 | 想象一下以前构造出来的 ReactComponent, 一旦构造出来就不好修改或重用, 比如还是前面的例子, 你有一个 Mult 控件, 做的是乘法. 92 | 93 | 那么如果我需要一个新的控件, 而且控件的也需要 `a * b`, 但是不同的是, 我们新控件的公式是 `(a * b)^2`. 要是修改函数我们思路很清晰, 构造个新函数呗: 94 | ```js 95 | function mmp(a, b) { 96 | return mult(mult(a, b), mult(a, b)) 97 | } 98 | ``` 99 | 100 | 如果是 React, 那就构造个 HoC 把 Mult 控件套进去呗. 101 | 102 | 但是不管是包函数还是包 React 控件, 都不是一个可持续组合的方式, 而且构造新的 React Class 又会需要很多啰嗦的模板代码, 如果你经常写 React 你知道我在说什么. 103 | 104 | Functor 可以免去这些复杂的过程, 而且提供了高可重用与组合的可能. 105 | 106 | 接着 `XMult`, 我们很容易能将其变换成 `XMMP` 107 | 108 | ```js 109 | let XMMP = XMult.map((s) => ({output: s.output * s.output})) 110 | ``` 111 | 112 | 这样我们就轻松从 XMult 生成一个新的 FantasyX, 在之前的基础上平方一下. 113 | 114 | 这就像操作数据一样简单 不是吗 115 | 116 | ```js 117 | [1,2,3].map(x=>x*x) 118 | // [2,4,6] 119 | ``` 120 | 121 | 完整代码: 122 | 123 | 124 | 125 | ## Monoid 126 | 127 | 有了变换, 我们可能还需要合并两个 FantasyX, 比如上面的 XMMP 和 XMult, 如果我需要一个同时能具有两种行为的 FantasyX 128 | 129 | ```js 130 | let XCOMBINE = XMMP.concat(XMult) 131 | ``` 132 | 133 | 仅此, 我们就得到了一个同时具有 XMMP 与 XMult 行为的 FantasyX 134 | 135 | 当然, 因为他们都会修改 `state.output`, 合并到一起会导致冲突, 我们稍微修改下 XMMP 136 | 137 | ```js 138 | let XMMP = XMult.map((s) => ({output2: s.output * s.output})) 139 | ``` 140 | 141 | 这样 View 就可以把 XMMP 和 Mult 的输出都显示出来 142 | 143 | 完整代码: 144 | 145 | 146 | 147 | ## Summary 148 | 149 | 使用 FantasyX, 我们很简单将逻辑从 UI 中分离 150 | 151 | - 写一个一般的简单函数, lift 之, 应用到 `xinput` 或者其他任何 `FantasyX` 类型 152 | - apply 到 View 上, 得到正常 ReactComponent 153 | 154 | 不但如此, 你还可以简单的变换或者合并 FantasyX 155 | 156 | - FantasyX 实现 Functor, 让你可以 从一个 FantasyX 轻松 map 到另一个 FantasyX 157 | - FantasyX 实现 Monoid, 简单的 concat 两个 FantasyX, 合成一个 FantasyX, 就跟数组 concat 一样. 158 | 159 | [更多...](http://xreact.oyanglul.us/) 160 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # xReact Examples 2 | 3 | these are examples of how to use xreact with rxjs/most, alacarte, typescript 4 | 5 | ## up and running 6 | ```sh 7 | yarn 8 | yarn 9 | ``` 10 | 11 | e.g. if you want to build todomvc example, `yarn todomvc` 12 | 13 | 14 | ## Live Demo 15 | - [alacarte](https://xreact.oyanglul.us/examples/alacart): use xreact with [**alacarte** data types](https://github.com/jcouyang/alacarte) 16 | - [counter](https://xreact.oyanglul.us/examples/counter/): the **simpliest** example in readme 17 | - [frp-counter](https://xreact.oyanglul.us/examples/frp-counter/): some **FRP** improvement of counter example 18 | - [todomvc](https://xreact.oyanglul.us/examples/todomvc/): a fully functional **TodoMVC** in TypeScript 19 | - [BMI Calculator](https://xreact.oyanglul.us/examples/bmi-calc/): compose xreact in **Fantasy Land** 20 | - [type-n-search](https://xreact.oyanglul.us/examples/type-n-search/): use xreact with [**mostjs**](https://github.com/cujojs/most) 21 | - [firebase](https://github.com/jcouyang/gulugulu): gulugulu **danmaku**, integrate with **firebase** with xreact 22 | -------------------------------------------------------------------------------- /examples/alacarte/app.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import X, { x } from 'xreact' 5 | import * as rx from 'xreact/lib/xs/rx' 6 | import '@reactivex/rxjs/dist/cjs/add/operator/filter' 7 | import '@reactivex/rxjs/dist/cjs/add/operator/do' 8 | import { 9 | Expr, 10 | Val, 11 | injectorFrom, 12 | interpreterFrom, 13 | interpretExpr, 14 | interpreterFor, 15 | isInjectedBy, 16 | } from 'alacarte.js' 17 | 18 | const compose = f => g => x=> f(g(x)) 19 | 20 | let {Add, Over} = Expr.create({ 21 | Add: ['fn'], 22 | Over: ['prop', 'fn'] 23 | }) 24 | 25 | // Instances of Interpreters 26 | const evalAdd = interpreterFor(Add, function (v) { 27 | return x => x + v.fn(x) 28 | }); 29 | 30 | const evalVal = interpreterFor(Val, function (v) { 31 | return ()=> v.value 32 | }); 33 | 34 | const evalOver = interpreterFor(Over, function (v) { 35 | let newstate = {} 36 | let prop = v.prop() 37 | return state => (newstate[prop] = v.fn(state[prop]), newstate) 38 | }); 39 | 40 | // You can define any Interpreters you want, instead of eval value, this interpreter print the expressions 41 | const printAdd = interpreterFor(Add, function (v) { 42 | return `(_ + ${v.fn})` 43 | }); 44 | 45 | const printVal = interpreterFor(Val, function (v) { 46 | return v.value.toString() 47 | }); 48 | 49 | const printOver = interpreterFor(Over, function (v) { 50 | return `over ${v.prop} do ${v.fn}` 51 | }); 52 | 53 | const printer = interpreterFrom([printVal, printAdd, printOver]) 54 | 55 | const CounterView = props => ( 56 |
57 | 58 | {props.count} 59 | 60 |
61 | ) 62 | 63 | CounterView.defaultProps = { count: 1 }; 64 | 65 | const counterable = x((intent$) => { 66 | // Compose a Interpreter which can interpret Lit, Add, Over 67 | let interpreter = interpreterFrom([evalVal, evalAdd, evalOver]) 68 | // Injector that can inject Lit, Add, Over 69 | let injector = injectorFrom([Val, Add, Over]) 70 | 71 | let [val, add, over] = injector.inject() 72 | 73 | return { 74 | update$: intent$.filter(isInjectedBy(injector)) // <-- filter only expressions compose with type Lit :+: Add :+: Over 75 | .do(compose(console.log)(interpretExpr(printer))) // interpret with printer 76 | .map(interpretExpr(interpreter)), // interpret with interpreter(eval value) 77 | actions: { 78 | inc: () => over(val('count'), add(val(1))), // you can compose expressions to achieve your bussiness 79 | dec: () => { 80 | let aNewInjector = injectorFrom([Val, Add, Over]) 81 | let [val, add, over] = aNewInjector.inject() 82 | return over(val('count'), add(val(-1))) 83 | } 84 | } // only a expr with same type and order can be interpret 85 | } 86 | }) 87 | 88 | // a new mult expr is add without modify any of the current code 89 | let {Mult} = Expr.create({ 90 | Mult: ['fn'], 91 | }) 92 | const evalMult = interpreterFor(Mult, function (v) { 93 | return x => x * v.fn(x) 94 | }); 95 | 96 | let printMult = interpreterFor(Mult, function (v) { 97 | return `(_ * ${v.fn})` 98 | }); 99 | 100 | const multable = x((intent$) => { 101 | let injector = injectorFrom([Val, Add, Over, Mult]) 102 | let [val, add, over, mult] = injector.inject() 103 | let interpreter = interpreterFrom([evalVal, evalAdd, evalOver, evalMult]) 104 | let printer = interpreterFrom([printVal, printAdd, printOver, printMult]) 105 | return { 106 | update$: intent$.filter(isInjectedBy(injector)) 107 | .do(compose(console.log)(interpretExpr(printer))) 108 | .map(interpretExpr(interpreter)), 109 | actions: { 110 | inc: () => over(val('count'), mult(val(2))), 111 | dec: () => over(val('count'), mult(val(0.5))), 112 | } 113 | } 114 | }) 115 | const Counter = counterable(CounterView) 116 | const Multer = multable(CounterView) 117 | render( 118 | 119 |
120 | 121 | 122 |
123 |
124 | , document.getElementById('app')); 125 | -------------------------------------------------------------------------------- /examples/alacarte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | counter 7 | 8 | 9 |

counter

10 |
11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/bmi-calc/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import * as RX from '../../lib/xs/rx' 4 | import { X, xinput, lift2, Actions } from '../..' 5 | 6 | // Types 7 | interface Intent extends Event { } 8 | 9 | interface BMIState { 10 | value: number 11 | weight: number 12 | height: number 13 | bmi: string 14 | health: string 15 | } 16 | 17 | interface BMIProps extends BMIState { 18 | actions: Actions; 19 | } 20 | 21 | // View 22 | const View: React.SFC> = props => ( 23 |
24 | 28 | 32 |

Your BMI is {props.bmi}

33 |

which means you're {props.health}

34 |
35 | ) 36 | 37 | View.defaultProps = { bmi: '', health: '' } 38 | 39 | // Plan 40 | const weightx = xinput<'number', RX.URI, Intent, BMIState>('weight') 41 | 42 | const heightx = xinput<'number', RX.URI, Intent, BMIState>('height') 43 | 44 | const BMIx = lift2( 45 | (s1, s2) => { 46 | let bmi = 0 47 | let health = '...' 48 | if (s1.weight && s2.height) { 49 | bmi = s1.weight / (s2.height * s2.height) 50 | } 51 | if (bmi < 18.5) health = 'underweight' 52 | else if (bmi < 24.9) health = 'normal' 53 | else if (bmi < 30) health = 'Overweight' 54 | else if (bmi >= 30) health = 'Obese' 55 | return { bmi: bmi.toString(), health } 56 | }) 57 | (weightx, heightx) 58 | 59 | const BMI = BMIx.apply(View) 60 | 61 | // BMIx is compose from weightx and heightx 62 | 63 | // while weightx and heightx can be still use to create another component 64 | const Weight = weightx.apply(props => props.weight ?

where weight is {props.weight} kg

: null) 65 | const Height = heightx.apply(props => props.height ?

and {props.height}m height

: null) 66 | render( 67 | 68 |
69 | 70 | 71 | 72 | 73 |
74 | , document.getElementById('app')); 75 | -------------------------------------------------------------------------------- /examples/bmi-calc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BMI Calculator 7 | 8 | 9 |

BMI Calculator

10 |
11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | ## run it from local 2 | ``` 3 | npm install 4 | npm run build 5 | npm start 6 | ``` 7 | See the [output](localhost:8000) 8 | ## exercise online 9 | [Counter Online](http://output.jsbin.com/waciku) 10 | Click "Edit in JS Bin" to see the source code. 11 | -------------------------------------------------------------------------------- /examples/counter/app.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import * as RX from '../../lib/xs/rx' 4 | import X, { x } from '../../lib/x' 5 | 6 | const CounterView = props => ( 7 |
8 | 9 | {props.count} 10 | 11 |
12 | ) 13 | CounterView.defaultProps = { count: 0 }; 14 | 15 | const counterable = x((intent$) => { 16 | return { 17 | update$: intent$.map(intent => { 18 | switch (intent.type) { 19 | case 'inc': 20 | return state => ({ count: state.count + 1 }); 21 | case 'dec': 22 | return state => ({ count: state.count - 1 }); 23 | default: 24 | return _ => _; 25 | } 26 | }), 27 | actions: { 28 | inc: () => ({ type: 'inc' }), 29 | dec: () => ({ type: 'dec' }), 30 | } 31 | } 32 | }) 33 | 34 | const Counter = counterable(CounterView) 35 | 36 | render( 37 | 38 | 39 | 40 | , document.getElementById('app')); 41 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | counter 7 | 8 | 9 |

counter

10 |
11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/frp-counter/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var React = require("react"); 4 | var react_dom_1 = require("react-dom"); 5 | var xreact_1 = require("xreact"); 6 | var rx = require("xreact/lib/xs/rx"); 7 | var when = require("when"); 8 | var most_1 = require("most"); 9 | var ramda_1 = require("ramda"); 10 | var Type = require("union-type"); 11 | var Intent = Type({ 12 | Inc: [Number], 13 | Dec: [Number], 14 | Double: [], 15 | Half: [] 16 | }); 17 | var CounterView = function (props) { return (React.createElement("div", null, 18 | React.createElement("button", { onClick: function () { return props.actions.half(); } }, "/2"), 19 | React.createElement("button", { onClick: function () { return props.actions.dec(1); } }, "-1"), 20 | React.createElement("span", null, props.count), 21 | React.createElement("button", { onClick: function () { return props.actions.inc(1); } }, "+1"), 22 | React.createElement("button", { onClick: function () { return props.actions.double(); } }, "*2"))); }; 23 | CounterView.defaultProps = { count: 0 }; 24 | var lensCount = ramda_1.lensProp('count'); 25 | var asyncInitCount11 = xreact_1.x(function (intent$) { 26 | return { 27 | update$: most_1.just(11) 28 | .flatMap(ramda_1.compose(most_1.fromPromise, when)) 29 | .map(ramda_1.set(lensCount)) 30 | }; 31 | }); 32 | var doublable = xreact_1.x(function (intent$) { 33 | return { 34 | update$: intent$.map(Intent.case({ 35 | Double: function () { return ramda_1.over(lensCount, function (x) { return x * 2; }); }, 36 | Half: function () { return ramda_1.over(lensCount, function (x) { return x / 2; }); }, 37 | _: function () { return ramda_1.identity; } 38 | })), 39 | actions: { 40 | double: function () { return Intent.Double; }, 41 | half: function () { return Intent.Half; }, 42 | } 43 | }; 44 | }); 45 | var increasable = xreact_1.x(function (intent$) { 46 | return { 47 | update$: intent$.map(Intent.case({ 48 | Inc: function (v) { return ramda_1.over(lensCount, function (x) { return x + v; }); }, 49 | Dec: function (v) { return ramda_1.over(lensCount, function (x) { return x - v; }); }, 50 | _: function () { return ramda_1.identity; } 51 | })), 52 | actions: { 53 | inc: Intent.Inc, 54 | dec: Intent.Dec, 55 | } 56 | }; 57 | }); 58 | var wrapper = ramda_1.compose(asyncInitCount11, doublable, increasable); 59 | var Counter = wrapper(CounterView); 60 | react_dom_1.render(React.createElement(xreact_1.default, { x: rx }, 61 | React.createElement(Counter, null)), document.getElementById('app')); 62 | -------------------------------------------------------------------------------- /examples/frp-counter/app.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import X, { x } from 'xreact' 4 | import * as rx from 'xreact/lib/xs/rx' 5 | import * as when from 'when' 6 | import {just, fromPromise} from 'most' 7 | import {compose, lensProp, over, set, identity} from 'ramda' 8 | import * as Type from 'union-type' 9 | const Intent = Type({ 10 | Inc: [Number], 11 | Dec: [Number], 12 | Double: [], 13 | Half: [] 14 | }) 15 | 16 | const CounterView = props => ( 17 |
18 | 19 | 20 | {props.count} 21 | 22 | 23 |
24 | ) 25 | 26 | CounterView.defaultProps = { count: 0 }; 27 | 28 | const lensCount = lensProp('count') 29 | 30 | const asyncInitCount11 = x(intent$=>{ 31 | return { 32 | update$: just(11) 33 | .flatMap(compose(fromPromise, when)) 34 | .map(set(lensCount)) 35 | } 36 | }) 37 | 38 | const doublable = x(intent$ => { 39 | return { 40 | update$: intent$.map(Intent.case({ 41 | Double: () => over(lensCount, x=>x*2), 42 | Half: () => over(lensCount, x=>x/2), 43 | _: () => identity 44 | })), 45 | actions: { 46 | double: ()=>Intent.Double, 47 | half: ()=>Intent.Half, 48 | } 49 | } 50 | }) 51 | 52 | const increasable = x(intent$ => { 53 | return { 54 | update$: intent$.map(Intent.case({ 55 | Inc: (v) => over(lensCount, x=>x+v), 56 | Dec: (v) => over(lensCount, x=>x-v), 57 | _: () => identity 58 | })), 59 | actions: { 60 | inc: Intent.Inc, 61 | dec: Intent.Dec, 62 | } 63 | } 64 | }) 65 | 66 | const wrapper = compose(asyncInitCount11, doublable, increasable) 67 | const Counter = wrapper(CounterView) 68 | 69 | render( 70 | 71 | 72 | 73 | , document.getElementById('app')); 74 | -------------------------------------------------------------------------------- /examples/frp-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FRP counter 7 | 8 | 9 |

FRP counter

10 |
11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/index.md: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xreact-examples", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Jichao OUYANG", 6 | "license": "MIT", 7 | "scripts": { 8 | "counter": "browserify -p tsify counter/app.jsx | uglifyjs > counter/app.js", 9 | "frp-counter": "browserify -p tsify frp-counter/app.jsx | uglifyjs > frp-counter/app.js", 10 | "type-n-search": "browserify -p tsify type-n-search/app.jsx | uglifyjs > type-n-search/app.js", 11 | "alacarte": "browserify -p tsify alacarte/app.jsx | uglifyjs > alacarte/app.js", 12 | "todomvc": "browserify -p tsify todomvc/app.tsx | uglifyjs > todomvc/app.js", 13 | "bmi-calc": "browserify -p tsify bmi-calc/app.tsx | uglifyjs > bmi-calc/app.js", 14 | "test": "jest" 15 | }, 16 | "dependencies": { 17 | "@reactivex/rxjs": "^5.4.0", 18 | "classnames": "^2.2.5", 19 | "most": "^1.4.0", 20 | "most-subject": "^5.3.0", 21 | "react": "^15.5.4", 22 | "react-dom": "^15.5.4", 23 | "xreact": "^2.0.2" 24 | }, 25 | "devDependencies": { 26 | "@types/classnames": "^2.2.0", 27 | "@types/node": "^7.0.22", 28 | "@types/react": "^15.0.25", 29 | "@types/react-dom": "^15.5.0", 30 | "alacarte.js": "^0.2.0", 31 | "browserify": "^14.3.0", 32 | "jest": "^20.0.4", 33 | "ramda": "^0.23.0", 34 | "rest": "^2.0.0", 35 | "tsify": "^3.0.1", 36 | "typescript": "^2.3.4", 37 | "uglify-js": "^3.0.11", 38 | "union-type": "^0.4.0", 39 | "when": "^3.7.8" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | https://reactive-react.github.io/react-most/examples/todomvc/public/ 2 | 3 | ``` 4 | npm install 5 | npm run build 6 | npm start 7 | ``` 8 | -------------------------------------------------------------------------------- /examples/todomvc/__mocks__/rest.js: -------------------------------------------------------------------------------- 1 | const when = require('when') 2 | let response 3 | const rest = jest.fn((url)=>{ 4 | return when({ 5 | entity: response 6 | }) 7 | }); 8 | 9 | rest.__return = function(resp){ 10 | response = resp 11 | }; 12 | export default rest 13 | -------------------------------------------------------------------------------- /examples/todomvc/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import X from 'xreact' 3 | import * as MOST from 'xreact/lib/xs/most' 4 | import Header from './components/Header'; 5 | import Footer from './components/Footer'; 6 | import MainSection from './components/MainSection'; 7 | import { render } from 'react-dom'; 8 | 9 | const App = () => ( 10 |
11 |
12 | 13 |
14 | ) 15 | 16 | render( 17 | 18 | 19 | 20 | , document.getElementById('app')); 21 | -------------------------------------------------------------------------------- /examples/todomvc/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as classnames from 'classnames' 3 | import MainSection from './MainSection' 4 | import * as Intent from '../intent' 5 | import { x } from 'xreact/lib/x' 6 | import * as StaticStream from 'xreact/lib/xs' 7 | import * as Most from 'xreact/lib/xs/most' 8 | import { Todo } from './interfaces' 9 | import { of } from 'most' 10 | import { identity } from 'ramda' 11 | 12 | const FILTER_TITLES = { 13 | 'SHOW_ALL': 'All', 14 | 'SHOW_ACTIVE': 'Active', 15 | 'SHOW_COMPLETED': 'Completed' 16 | } 17 | 18 | export const FILTER_FUNC = { 19 | 'SHOW_ALL': identity, 20 | 'SHOW_ACTIVE': (todos: Todo[]) => todos.filter(todo => !todo.done), 21 | 'SHOW_COMPLETED': (todos: Todo[]) => todos.filter(todo => todo.done), 22 | } 23 | const FilterLink = ({ onClick, filter, current }) => { 24 | return
  • 25 | 28 | {FILTER_TITLES[filter]} 29 | 30 |
  • 31 | } 32 | 33 | const TodoCount = ({ activeCount }) => ( 34 | 35 | {activeCount || 'No '} 36 | {activeCount === 1 ? 'item' : 'items'} left 37 | 38 | ) 39 | 40 | const ClearButton = ({ completedCount, actions }) => ( 41 | completedCount > 0 ? 42 | : null 46 | ) 47 | const Footer = React.createClass({ 48 | getInitialState() { 49 | return { 50 | currentFilter: 'SHOW_ALL' 51 | } 52 | }, 53 | 54 | render() { 55 | return ( 56 |
    57 | 58 |
      59 | {['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED'].map(filter => 60 | { 65 | this.setState({ currentFilter: filter }) 66 | this.props.actions.filterWith(FILTER_FUNC[filter]) 67 | }} /> 68 | )} 69 |
    70 | 71 |
    72 | ) 73 | }, 74 | }); 75 | export default x, Todo>((intent$) => { 76 | return { 77 | update$: of(identity), 78 | actions: { 79 | clear: () => ({ kind: 'clear' } as Intent.Clear), 80 | filterWith: (f) => ({ kind: 'filter', filter: f } as Intent.Filter), 81 | } 82 | } 83 | })(Footer) 84 | -------------------------------------------------------------------------------- /examples/todomvc/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | const Header = () => { 5 | return ( 6 |
    7 |

    todos

    8 | 11 |
    12 | ) 13 | } 14 | export default Header 15 | -------------------------------------------------------------------------------- /examples/todomvc/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TodoItem from './TodoItem' 3 | import Footer from './Footer' 4 | import { x } from 'xreact/lib/x' 5 | import * as Most from 'xreact/lib/xs/most' 6 | import { Subject } from 'most-subject' 7 | import { Stream } from 'most' 8 | import { Intent } from '../intent' 9 | import * as r from 'ramda' 10 | const alwaysId = () => r.identity 11 | import * as most from 'most' 12 | interface Todo { 13 | id: number 14 | text: string 15 | done: boolean 16 | } 17 | interface MainSectionProps { 18 | todos: Todo[] 19 | filter: (todos: Todo[]) => Todo[] 20 | } 21 | const MainSection: React.StatelessComponent = ({ todos, filter }) => { 22 | const completedCount = todos.reduce((count, todo) => todo.done ? count + 1 : count, 0); 23 | const filteredTodos = filter(todos); 24 | return ( 25 |
    26 | 27 | 28 |
      29 | {filteredTodos.map((todo, index) => 30 | 31 | )} 32 |
    33 |
    34 |
    35 | ) 36 | } 37 | 38 | MainSection.defaultProps = { 39 | todos: [], 40 | filter: (_: Todo[]) => _ 41 | } 42 | 43 | export default x, MainSectionProps>((intent$) => { 44 | let lensTodos = r.lensProp('todos') 45 | let lensComplete = r.lensProp('done') 46 | let lensTodo = (index: number) => 47 | r.compose(lensTodos, r.lensIndex(index)) 48 | let lensTodoComplete = (index: number) => 49 | r.compose(lensTodo(index), lensComplete) 50 | let nextId = r.compose(r.last, 51 | r.map((x: Todo) => x.id + 1), 52 | r.sortBy(r.prop('id'))) 53 | let update$ = intent$.map((intent: Intent) => { 54 | switch (intent.kind) { 55 | case ('add'): 56 | return (state: MainSectionProps) => { 57 | let nextid = nextId(state.todos) || 0 58 | return r.over(lensTodos, r.append(r.assoc('id', nextid, intent.value)), state) 59 | } 60 | case 'edit': 61 | return r.set(lensTodo(intent.todo.id), intent.todo) 62 | case 'clear': 63 | return r.over(lensTodos, r.filter((todo: Todo) => !todo.done)) 64 | case 'delete': 65 | return r.over(lensTodos, r.filter((todo: Todo) => todo.id != intent.id)) 66 | case 'filter': 67 | return (state: MainSectionProps) => ({ filter: intent.filter }) 68 | case 'done': 69 | return r.over(lensTodoComplete(intent.index), r.not) 70 | default: 71 | return (state: MainSectionProps) => state 72 | } 73 | }) 74 | 75 | let data$ = most.fromPromise(Promise.resolve(require('../todos.json'))) 76 | .map(state => () => state) 77 | 78 | return { 79 | update$: update$.merge(data$), 80 | } 81 | })(MainSection) 82 | -------------------------------------------------------------------------------- /examples/todomvc/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as classnames from 'classnames' 3 | import TodoTextInput from './TodoTextInput' 4 | import MainSection from './MainSection' 5 | import { x } from 'xreact/lib/x' 6 | import * as Most from 'xreact/lib/xs/most' 7 | import * as Intent from '../intent' 8 | import { Todo } from './interfaces' 9 | import { just } from 'most' 10 | import { identity as id } from 'ramda' 11 | const TodoItemView = ({ todo, actions, index }) => { 12 | return
    13 | actions.done(index)} /> 17 | 20 |
    23 | } 24 | 25 | const TodoItem = props => { 26 | const { todo, actions, editing, index } = props 27 | const { edit } = actions 28 | let element = editing === todo.id ? : 33 | 34 | return
  • {element}
  • 38 | } 39 | 40 | const intentWrapper = x, {}>((intent$) => { 41 | return { 42 | update$: just(id), 43 | actions: { 44 | add: () => ({ kind: 'add' } as Intent.Add), 45 | edit: (todo) => ({ kind: 'edit', todo } as Intent.Edit), 46 | done: (index) => ({ kind: 'done', index } as Intent.Done), 47 | remove: (id) => ({ kind: 'delete', id } as Intent.Delete), 48 | } 49 | } 50 | }) 51 | export default intentWrapper(TodoItem) 52 | -------------------------------------------------------------------------------- /examples/todomvc/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Component, PropTypes } from 'react' 3 | import * as classnames from 'classnames' 4 | import MainSection from './MainSection' 5 | import { x } from 'xreact/lib/x' 6 | import TodoItem from './TodoItem' 7 | import * as Intent from '../intent' 8 | import { Todo } from './interfaces' 9 | import { just } from 'most' 10 | import { identity as id } from 'ramda' 11 | let TodoTextInput = React.createClass({ 12 | getInitialState() { 13 | return { 14 | text: this.props.text || '' 15 | } 16 | }, 17 | 18 | handleSubmit(e) { 19 | const text = e.target.value.trim() 20 | let msg = { id: this.props.itemid, text: text } 21 | if (e.which === 13) { 22 | if (this.props.newTodo) { 23 | this.props.actions.add(msg); 24 | this.setState({ text: '' }) 25 | } 26 | } 27 | }, 28 | 29 | handleChange(e) { 30 | this.setState({ text: e.target.value }) 31 | }, 32 | 33 | handleBlur(e) { 34 | if (!this.props.newTodo) { 35 | this.props.actions.edit({ id: this.props.itemid, text: e.target.value }, this.props.index); 36 | this.props.actions.editing(-1); 37 | } 38 | }, 39 | 40 | render() { 41 | return ( 42 | 54 | ) 55 | }, 56 | }); 57 | 58 | export default x(intent$ => ( 59 | { 60 | update$: just(id), 61 | actions: { 62 | add: (value) => ({ kind: 'add', value } as Intent.Add) 63 | } 64 | } 65 | ))(TodoTextInput) 66 | -------------------------------------------------------------------------------- /examples/todomvc/components/__tests__/MainSection-spec.jsx: -------------------------------------------------------------------------------- 1 | jest.mock('rest') 2 | import rest from 'rest' 3 | import React from 'react' 4 | import when from 'when' 5 | import Intent from '../../intent' 6 | import MainSection from '../MainSection.jsx' 7 | import Footer, {FILTER_FUNC} from '../Footer.jsx' 8 | import TodoItem from '../TodoItem.jsx' 9 | import Most from 'react-most' 10 | import {stateStreamOf, stateHistoryOf, 11 | intentStreamOf, intentHistoryOf, 12 | run, dispatch, 13 | Engine } from 'react-most-spec'; 14 | import TestUtils from 'react-addons-test-utils' 15 | 16 | describe('MainSection', ()=>{ 17 | const RESPONSE = '[{"text": "Try React Most","done": false,"id": 0},{"text": "Give it a Star on Github","done": false,"id": 1}]' 18 | beforeEach(()=>{ 19 | rest.__return(RESPONSE) 20 | }) 21 | describe('View', ()=> { 22 | const defaultTodos = [ 23 | {id:0, text:'Loading...dadada', done:false}, 24 | {id:1, text:'dadada', done:false}, 25 | {id:2, text:'yay', done:true}, 26 | ] 27 | it('should show correct counts on footer', ()=> { 28 | let mainSection = TestUtils.renderIntoDocument( 29 | 30 | _} 33 | /> 34 | 35 | ) 36 | let footer = TestUtils.findRenderedComponentWithType(mainSection, Footer); 37 | expect(footer.props.completedCount).toBe(1) 38 | expect(footer.props.activeCount).toBe(2) 39 | }) 40 | 41 | it('should show only active todos', ()=> { 42 | let mainSection = TestUtils.renderIntoDocument( 43 | 44 | 48 | 49 | ) 50 | let todoItems = TestUtils.scryRenderedComponentsWithType(mainSection, TodoItem); 51 | expect(todoItems.map(item=>item.props.todo.id)).toEqual( 52 | [0,1]) 53 | }) 54 | 55 | it('should show only done todos', ()=> { 56 | let mainSection = TestUtils.renderIntoDocument( 57 | 58 | 62 | 63 | ) 64 | let todoItems = TestUtils.scryRenderedComponentsWithType(mainSection, TodoItem); 65 | expect(todoItems.map(item=>item.props.todo.id)).toEqual( 66 | [2]) 67 | }) 68 | }) 69 | 70 | describe('Behavior', ()=> { 71 | let mainSectionWrapper, mainSection, send 72 | 73 | describe('data sink', ()=>{ 74 | beforeEach(()=>{ 75 | mainSectionWrapper = TestUtils.renderIntoDocument( 76 | 77 | 78 | 79 | ) 80 | mainSection = TestUtils.findRenderedComponentWithType(mainSectionWrapper, MainSection); 81 | }) 82 | it('should render default state', ()=>{ 83 | expect(mainSection.state.todos).toEqual([ 84 | {id:0, text:'Loading...dadada', done:false}, 85 | ]) 86 | }) 87 | it('should get data from rest response to MainSection', ()=>{ 88 | return run(stateStreamOf(mainSection), 89 | dispatch([], mainSection), 90 | [ 91 | state=>expect(state.todos).toEqual(JSON.parse(RESPONSE)) 92 | ]) 93 | }) 94 | }); 95 | 96 | describe('sync', ()=>{ 97 | beforeEach(()=>{ 98 | mainSectionWrapper = TestUtils.renderIntoDocument( 99 | 100 | 101 | 102 | ) 103 | mainSection = TestUtils.findRenderedComponentWithType(mainSectionWrapper, MainSection); 104 | }) 105 | describe('edit', ()=>{ 106 | it('should update todo id 0 item text', ()=>{ 107 | return dispatch([ 108 | Intent.Edit({id:0, text:'heheda0'}, 0), 109 | Intent.Done(0), 110 | Intent.Clear(), 111 | Intent.Add({text:'heheda1'}), 112 | Intent.Delete(0), 113 | Intent.Filter(FILTER_FUNC['SHOW_COMPLETED']), 114 | ], mainSection).then(()=>{ 115 | let state = stateHistoryOf(mainSection) 116 | expect(state[0].todos[0]).toEqual({"id": 0, "text": "heheda0"}) 117 | expect(state[1].todos[0]).toEqual({"id": 0, "text": "heheda0", "done": true}) 118 | expect(state[2].todos).toEqual([]) 119 | expect(state[3].todos[0]).toEqual({"id": 0, "text": "heheda1"}) 120 | expect(state[4].todos).toEqual([]) 121 | expect(state[5].filter).toEqual(FILTER_FUNC['SHOW_COMPLETED']) 122 | }) 123 | }) 124 | }); 125 | }) 126 | }) 127 | }); 128 | -------------------------------------------------------------------------------- /examples/todomvc/components/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: number 3 | text: string 4 | done: boolean 5 | } 6 | export interface MainSectionProps { 7 | todos: Todo[] 8 | filter: (todos: Todo[]) => Todo[] 9 | } 10 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Template • TodoMVC 7 | 8 | 9 | 10 | 11 |
    12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/todomvc/intent.ts: -------------------------------------------------------------------------------- 1 | export interface Add { 2 | kind: 'add' 3 | value: S 4 | } 5 | export interface Delete { 6 | kind: 'delete' 7 | id: number 8 | } 9 | export interface Edit { 10 | kind: 'edit' 11 | todo: S 12 | } 13 | export interface Clear { 14 | kind: 'clear' 15 | } 16 | export interface Filter { 17 | kind: 'filter' 18 | filter: (xs: S[]) => S[] 19 | } 20 | 21 | export interface Done { 22 | kind: 'done' 23 | index: number 24 | } 25 | export interface Complete { 26 | kind: 'complete' 27 | } 28 | 29 | export type Intent = Add | Delete | Edit | Clear | Filter | Done | Complete 30 | -------------------------------------------------------------------------------- /examples/todomvc/todos.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "text": "Try React Most", 3 | "done": true, 4 | "id": 0 5 | },{ 6 | "text": "Give it a Star on Github", 7 | "done": false, 8 | "id": 1 9 | }] 10 | -------------------------------------------------------------------------------- /examples/todomvc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "include": [ 2 | "src/**/*" 3 | ], 4 | 5 | "compilerOptions": { 6 | /* Basic Options */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation: */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "dist", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true /* Enable all strict type-checking options. */ 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 29 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 30 | 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | 37 | /* Module Resolution Options */ 38 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 42 | // "typeRoots": [], /* List of folders to include type definitions from. */ 43 | // "types": [], /* Type declaration files to be included in compilation. */ 44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./**/app.tsx", 4 | "./**/app.jsx" 5 | ], 6 | 7 | "compilerOptions": { 8 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ 9 | "lib": ["dom", "es2015"], /* Specify library files to be included in the compilation: */ 10 | "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": false, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 29 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 30 | 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | 37 | /* Module Resolution Options */ 38 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 42 | // "typeRoots": ["./node_modules/@types"], /* List of folders to include type definitions from. */ 43 | "types": ["node"], /* Type declaration files to be included in compilation. */ 44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | "target": "es5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/type-n-search/README.md: -------------------------------------------------------------------------------- 1 | # Github Repo Type N Search Example 2 | 3 | https://reactive-react.github.io/react-most/examples/type-n-search/public/ 4 | 5 | ## install 6 | ```sh 7 | npm install 8 | npm run build 9 | npm start 10 | ``` 11 | 12 | ## Code Walk Through 13 | 14 | it's really simple example(only 40 LOC) that reactively search github repo according to your query 15 | 16 | 0. create normal React Component 17 | 18 | ```js 19 | const TypeNsearch = (props)=>{ 20 | let {search} = props.actions 21 | return
    22 | search(e.target.value)}> 23 | 28 |
    29 | } 30 | ``` 31 | 32 | 1. HOC(Higher Order Component) 33 | using `connect` to create a HOC over TypeNsearch 34 | 35 | ```js 36 | const MostTypeNSearch = connect(DATAFLOW)(TypeNsearch) 37 | ``` 38 | 2. Compose Dataflow 39 | you see the place holder `DATAFLOW`, now we gonna fill in the real data flow how we enable the reactive action of our Component 40 | 1. filter out stream only with `intent.type` of 'search' 41 | 42 | ```js 43 | function(intent$){ 44 | let updateSink$ = intent$.filter(i=>i.type=='search') 45 | .debounce(500) 46 | ... 47 | ``` 48 | using `debounce` will transform the stream to stream which only bouncing at certain time point 49 | ``` 50 | --冷-冷笑--冷笑话--> 51 | 52 | --------冷笑话--> 53 | ``` 54 | 2. compose a VALID query URL 55 | 56 | ```js 57 | ... 58 | .map(intent=>intent.value) 59 | .filter(query=>query.length > 0) 60 | .map(query=>GITHUB_SEARCH_API + query) 61 | ... 62 | ``` 63 | 3. flatMap the Response to our stream 64 | ```js 65 | .flatMap(url=>most.fromPromise( 66 | rest(url).then(resp=>({ 67 | type: 'dataUpdate', 68 | value: resp.entity 69 | })))) 70 | ``` 71 | 72 | `flatMap` is simply just `map` and then `flat` 73 | 74 | > just pretent one `-` as one sec 75 | 76 | ``` 77 | intentStream --urlA---urlB---> 78 | rest(urlA) -------respA----> 79 | rest(urlB) ---------respB--> 80 | flatMap(rest)-------respA--respB---> 81 | ``` 82 | 4. model 83 | now our intent stream become a data stream, let's make it a modle stream. 84 | ``` js 85 | .filter(i=>i.type=='dataUpdate') 86 | .map(data=>JSON.parse(data.value).items) 87 | .map(items=>items.slice(0,10)) 88 | ``` 89 | parse it to JS Object and only get the first ten results 90 | 5. create state transforming stream 91 | ``` 92 | .map(items=>state=>({results: items})) 93 | ``` 94 | 95 | ``` 96 | modleStream ---mA---mB---> 97 | stateStream ---state=>({results:mA})---state=>({results:mB})---> 98 | ``` 99 | 100 | 3. return `actions` and `sinks` 101 | ```js 102 | return { 103 | search: value=>({type:'search',value}), 104 | updateSink$, 105 | } 106 | ``` 107 | return `search` then you can use `props.actions.search` in your Component 108 | 109 | return `updateSink$` then it can be appled to HOC's state, HOC will pass the state to your Component as props 110 | -------------------------------------------------------------------------------- /examples/type-n-search/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var xreact_1 = require("xreact"); 4 | var react_dom_1 = require("react-dom"); 5 | var React = require("react"); 6 | var most = require("most"); 7 | var rest = require("rest"); 8 | var MOST = require("xreact/lib/xs/most"); 9 | var GITHUB_SEARCH_API = 'https://api.github.com/search/repositories?q='; 10 | var TypeNsearch = function (props) { 11 | var search = props.actions.search; 12 | var error = props.error || {}; 13 | return React.createElement("div", null, 14 | React.createElement("input", { onChange: function (e) { return search(e.target.value); } }), 15 | React.createElement("span", { className: "red " + error.className }, error.message), 16 | React.createElement("ul", null, props.results.map(function (item) { 17 | return React.createElement("li", { key: item.id }, 18 | React.createElement("a", { href: item.html_url }, 19 | item.full_name, 20 | " (", 21 | item.stargazers_count, 22 | ")")); 23 | }))); 24 | }; 25 | TypeNsearch.defaultProps = { 26 | results: [] 27 | }; 28 | var log = function (x) { return console.log(x); }; 29 | var MostTypeNSearch = xreact_1.x(function (intent$) { 30 | var updateSink$ = intent$.filter(function (i) { return i.type == 'search'; }) 31 | .debounce(500) 32 | .map(function (intent) { return intent.value; }) 33 | .filter(function (query) { return query.length > 0; }) 34 | .map(function (query) { return GITHUB_SEARCH_API + query; }) 35 | .map(function (url) { return rest(url).then(function (resp) { return ({ 36 | type: 'dataUpdate', 37 | value: resp.entity 38 | }); }).catch(function (error) { 39 | console.error('API REQUEST ERROR:', error); 40 | return { 41 | type: 'dataError', 42 | value: error.message 43 | }; 44 | }); }) 45 | .flatMap(most.fromPromise) 46 | .filter(function (i) { return i.type == 'dataUpdate'; }) 47 | .map(function (data) { return JSON.parse(data.value).items; }) 48 | .map(function (items) { return items.slice(0, 10); }) 49 | .map(function (items) { return function (state) { return ({ results: items }); }; }) 50 | .flatMapError(function (error) { 51 | console.log('[CRITICAL ERROR]:', error); 52 | return most.of({ message: error.error, className: 'display' }) 53 | .merge(most.of({ className: 'hidden' }).delay(3000)) 54 | .map(function (error) { return function (state) { return ({ error: error }); }; }); 55 | }); 56 | return { 57 | actions: { 58 | search: function (value) { return ({ type: 'search', value: value }); }, 59 | }, 60 | update$: updateSink$ 61 | }; 62 | })(TypeNsearch); 63 | react_dom_1.render(React.createElement(xreact_1.default, { x: MOST }, 64 | React.createElement(MostTypeNSearch, null)), document.getElementById('app')); 65 | -------------------------------------------------------------------------------- /examples/type-n-search/app.jsx: -------------------------------------------------------------------------------- 1 | import X, {x} from 'xreact' 2 | import {render} from 'react-dom' 3 | import * as React from 'react' 4 | import * as most from 'most' 5 | import * as rest from 'rest' 6 | import * as MOST from 'xreact/lib/xs/most' 7 | 8 | const GITHUB_SEARCH_API = 'https://api.github.com/search/repositories?q='; 9 | const TypeNsearch = (props)=>{ 10 | let {search} = props.actions 11 | let error = props.error||{} 12 | return
    13 | search(e.target.value)}> 14 | {error.message} 15 | 20 |
    21 | } 22 | TypeNsearch.defaultProps = { 23 | results: [] 24 | } 25 | const log = x=>console.log(x) 26 | const MostTypeNSearch = x(function(intent$){ 27 | let updateSink$ = intent$.filter(i=>i.type=='search') 28 | .debounce(500) 29 | .map(intent=>intent.value) 30 | .filter(query=>query.length > 0) 31 | .map(query=>GITHUB_SEARCH_API + query) 32 | .map(url=>rest(url).then(resp=>({ 33 | type: 'dataUpdate', 34 | value: resp.entity 35 | })).catch(error=>{ 36 | console.error('API REQUEST ERROR:', error) 37 | return { 38 | type: 'dataError', 39 | value: error.message 40 | } 41 | })) 42 | .flatMap(most.fromPromise) 43 | .filter(i=>i.type=='dataUpdate') 44 | .map(data=>JSON.parse(data.value).items) 45 | .map(items=>items.slice(0,10)) 46 | .map(items=>state=>({results: items})) 47 | .flatMapError(error=>{ 48 | console.log('[CRITICAL ERROR]:', error); 49 | return most.of({message:error.error,className:'display'}) 50 | .merge(most.of({className:'hidden'}).delay(3000)) 51 | .map(error=>state=>({error})) 52 | }) 53 | 54 | return { 55 | actions: { 56 | search: value=>({type:'search',value}), 57 | }, 58 | update$: updateSink$ 59 | } 60 | })(TypeNsearch); 61 | 62 | render( 63 | 64 | , document.getElementById('app')); 65 | -------------------------------------------------------------------------------- /examples/type-n-search/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Type N Search 7 | 18 | 19 | 20 |

    Github Reactive Repo Search

    21 |
    22 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/type-n-search/src/transducer.jsx: -------------------------------------------------------------------------------- 1 | import {map, filter, comp, mapcat} from 'transducers-js' 2 | import Most,{connect} from 'react-most' 3 | import ReactDOM from 'react-dom' 4 | import React from 'react' 5 | import * as most from 'most' 6 | import rest from 'rest' 7 | const GITHUB_SEARCH_API = 'https://api.github.com/search/repositories?q='; 8 | const TypeNsearch = (props)=>{ 9 | let {search} = props.actions 10 | let error = props.error||{} 11 | return
    12 | search(e.target.value)}> 13 | {error.message} 14 | 19 |
    20 | } 21 | 22 | TypeNsearch.defaultProps = { 23 | results: [] 24 | } 25 | const sendApiRequest = comp( 26 | map(i=>i.value), 27 | filter(q=>q.length>0), 28 | map(q=>GITHUB_SEARCH_API + q), 29 | map(url=>rest(url).then(resp=>({ 30 | type: 'dataUpdate', 31 | value: resp.entity 32 | }))) 33 | ); 34 | 35 | const generateStateFromResp = comp( 36 | filter(i=>i.type=='dataUpdate'), 37 | map(data=>JSON.parse(data.value).items), 38 | map(items=>items.slice(0,10)), 39 | map(items=>state=>({results: items})) 40 | ) 41 | 42 | const log = x=>console.log(x) 43 | const MostTypeNSearch = connect(function(intent$){ 44 | let updateSink$ = intent$.filter(i=>i.type=='search') 45 | .debounce(500) 46 | .transduce(sendApiRequest) 47 | .flatMap(most.fromPromise) 48 | .transduce(generateStateFromResp) 49 | .flatMapError(error=>{ 50 | console.log('[ERROR]:', error); 51 | return most.of({message:error.error,className:'display'}) 52 | .merge(most.of({className:'hidden'}).delay(3000)) 53 | .map(error=>state=>({error})) 54 | }) 55 | 56 | return { 57 | actions:{ 58 | search: value=>({type:'search',value}), 59 | }, 60 | update$: updateSink$, 61 | } 62 | })(TypeNsearch); 63 | 64 | ReactDOM.render( 65 | 66 | , document.getElementById('app')); 67 | -------------------------------------------------------------------------------- /examples/type-n-search/src/undo.jsx: -------------------------------------------------------------------------------- 1 | import Most,{connect} from 'react-most' 2 | import ReactDOM from 'react-dom' 3 | import React from 'react' 4 | import * as most from 'most' 5 | import rest from 'rest' 6 | const GITHUB_SEARCH_API = 'https://api.github.com/search/repositories?q='; 7 | 8 | const TypeNsearch = (props)=>{ 9 | let {search} = props.actions 10 | let error = props.error||{} 11 | return
    12 | 13 | 14 | search(e.target.value)}> 15 | {error.message} 16 | 21 |
    22 | } 23 | 24 | const MostTypeNSearch = connect(function(intent$){ 25 | let undoSink$ = intent$.filter(i=>i.type=='undo') 26 | ; 27 | let updateSink$ = intent$.filter(i=>i.type=='search') 28 | .debounce(500) 29 | .map(intent=>intent.value) 30 | .filter(query=>query.length > 0) 31 | .map(query=>GITHUB_SEARCH_API + query) 32 | .map(url=>rest(url).then(resp=>({ 33 | type: 'dataUpdate', 34 | value: resp.entity 35 | }))) 36 | .flatMap(most.fromPromise) 37 | .filter(i=>i.type=='dataUpdate') 38 | .map(data=>JSON.parse(data.value).items) 39 | .map(items=>items.slice(0,10)) 40 | .map(items=>state=>({results: items})) 41 | .flatMapError(error=>{ 42 | console.log('[ERROR]:', error); 43 | return most.of({message:error.error,className:'display'}) 44 | .merge(most.of({className:'hidden'}).delay(3000)) 45 | .map(error=>state=>({error})) 46 | }) 47 | 48 | return { 49 | actions:{ 50 | search: value=>({type:'search',value}), 51 | }, 52 | update$: updateSink$, 53 | } 54 | }, {history: true})(TypeNsearch); 55 | 56 | ReactDOM.render( 57 | 58 | , document.getElementById('app')); 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xreact", 3 | "version": "5.0.1", 4 | "description": "Functional Reactive State Container for React", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/jcouyang/react-most.git" 8 | }, 9 | "main": "lib/index.js", 10 | "typings": "lib/index.d.ts", 11 | "files": [ 12 | "src", 13 | "lib", 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "make all", 18 | "test": "jest", 19 | "prepublish": "npm run build", 20 | "testWDebugger": "node --harmony $(which bugger) ./node_modules/jest-cli/bin/jest.js --runInBand" 21 | }, 22 | "browserify-shim": { 23 | "react": "global:React", 24 | "@reactivex/rxjs": "global:Rx", 25 | "most": "global:most" 26 | }, 27 | "dependencies": { 28 | "prop-types": "^15.6.0", 29 | "reflect-metadata": "^0.1.12" 30 | }, 31 | "peerDependencies": { 32 | "react": "^16.2.0" 33 | }, 34 | "optionalDependencies": { 35 | "@reactivex/rxjs": "^5.5.6", 36 | "most": "^1.7.2", 37 | "most-subject": "^5.3.0" 38 | }, 39 | "devDependencies": { 40 | "@reactivex/rxjs": "^5.5.6", 41 | "@types/jest": "^22.1.0", 42 | "@types/node": "^9.3.0", 43 | "@types/prop-types": "^15.5.2", 44 | "@types/react": "^16.0.34", 45 | "browserify": "^15.2.0", 46 | "browserify-shim": "^3.8.14", 47 | "chai": "^4.1.2", 48 | "create-react-class": "^15.6.2", 49 | "envify": "^4.1.0", 50 | "enzyme": "^3.3.0", 51 | "enzyme-adapter-react-16": "^1.1.1", 52 | "jest": "^22.1.4", 53 | "lodash": "^4.0.0", 54 | "mocha": "^5.0.0", 55 | "most": "^1.7.2", 56 | "most-subject": "^5.3.0", 57 | "nightmare": "^2.10.0", 58 | "react": "^16.2.0", 59 | "react-dom": "^16.2.0", 60 | "react-test-renderer": "^16.2.0", 61 | "redux": "^3.0.4", 62 | "ts-jest": "^22.0.1", 63 | "tsify": "^3.0.4", 64 | "typescript": "^2.7.1", 65 | "uglify-js": "^3.3.8", 66 | "watchify": "^3.9.0" 67 | }, 68 | "jest": { 69 | "transform": { 70 | "^.+\\.(tsx|ts)$": "/node_modules/ts-jest/preprocessor.js" 71 | }, 72 | "collectCoverageFrom": [ 73 | "src/x.ts", 74 | "src/xclass.ts" 75 | ], 76 | "moduleFileExtensions": [ 77 | "ts", 78 | "tsx", 79 | "js" 80 | ], 81 | "testMatch": [ 82 | "**/__tests__/*.(ts|tsx|js)" 83 | ], 84 | "roots": [ 85 | "src" 86 | ], 87 | "coverageDirectory": "./coverage/", 88 | "collectCoverage": true 89 | }, 90 | "author": "Jichao Ouyang", 91 | "license": "MIT", 92 | "keywords": [ 93 | "Rx", 94 | "RxJS", 95 | "ReactiveX", 96 | "ReactiveExtensions", 97 | "Streams", 98 | "Observables", 99 | "Observable", 100 | "Stream", 101 | "React", 102 | "most", 103 | "mostjs", 104 | "FRP", 105 | "Reactive", 106 | "ES6", 107 | "ES2015", 108 | "xreact", 109 | "typescript" 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.16 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.47deg" % "sbt-microsites" % "0.6.3") 2 | -------------------------------------------------------------------------------- /src/__tests__/fantasy-test.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '../fantasy/typeclasses/id' 2 | import { Functor, map } from '../fantasy/typeclasses/functor' 3 | 4 | import { concat } from '../fantasy/typeclasses/semigroup' 5 | import { Xstream } from '../fantasy/xstream' 6 | 7 | describe('fantasy', () => { 8 | describe('functor ID', () => { 9 | it('map just apply ', () => { 10 | expect(map((a: number) => a + 1, new Id(23)).valueOf()).toEqual(24) 11 | }) 12 | }) 13 | 14 | describe('functor Array', () => { 15 | it('map', () => { 16 | expect(map((a: number) => a + 1, [1, 2, 3])).toEqual([2, 3, 4]) 17 | }) 18 | }) 19 | }) 20 | 21 | describe('semigroup', () => { 22 | describe('concat', () => { 23 | it('String', () => { 24 | expect(concat("hello", "world")).toEqual("helloworld") 25 | }) 26 | 27 | it('Array', () => { 28 | expect(concat(["hello"], ["world"])).toEqual(["hello", "world"]) 29 | }) 30 | 31 | it('Number', () => { 32 | expect(concat(1, 2)).toEqual(3) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/__tests__/fantasyx-test.ts: -------------------------------------------------------------------------------- 1 | import { streamOps } from '../xs' 2 | import '../xs/array' 3 | import { FantasyX } from '../fantasy/fantasyx' 4 | import { Id } from '../fantasy/typeclasses/id' 5 | import { Update } from '../interfaces' 6 | import { Xstream } from '../fantasy/xstream' 7 | import { flatMap, FlatMap } from '../fantasy/typeclasses/flatmap' 8 | import { concat, Semigroup } from '../fantasy/typeclasses/semigroup' 9 | describe('FantasyX', () => { 10 | let intent; 11 | beforeEach(() => { 12 | intent = streamOps.subject() 13 | }) 14 | it('map and fold', () => { 15 | intent.next(1) 16 | intent.next(2) 17 | let res = Xstream 18 | .fromIntent() 19 | .toFantasyX() 20 | .map(a => { 21 | if (a == 1) { 22 | return { count: 5 } 23 | } else { 24 | return { count: 7 } 25 | } 26 | }) 27 | .foldS((s, a) => ({ count: a.count + s.count })) 28 | .toStream(intent) 29 | .reduce((acc, f) => f(acc), { count: 10 }) 30 | expect(res).toEqual({ count: 10 + 5 + 7 }) 31 | }) 32 | 33 | it('flatMap Xstream', () => { 34 | intent.next(1) 35 | intent.next(2) 36 | 37 | let res = FlatMap.Xstream.flatMap( 38 | (x: number) => Xstream.from(new Id({ count: x + 1 })) 39 | , Xstream.fromIntent<"ArrayStream", number>()) 40 | .toFantasyX() 41 | .toStream(intent) 42 | .reduce((acc, f: any) => f(acc), { count: 10 }) 43 | expect(res).toEqual({ count: 3 }) 44 | }) 45 | 46 | it('concat object', () => { 47 | intent.next({ count1: 1 }) 48 | intent.next({ count2: 2 }) 49 | 50 | let res = Semigroup.Xstream.concat( 51 | Xstream.fromIntent() 52 | , Xstream.fromIntent()) 53 | .toFantasyX() 54 | .toStream(intent) 55 | .reduce((acc, f: any) => f(acc), { count: 0 }) 56 | expect(res).toEqual({ "count": 0, "count1": 1, "count2": 2 }) 57 | }) 58 | 59 | it('concat promise', () => { 60 | 61 | let res = Semigroup.Xstream.concat( 62 | Xstream.from(new Id({ count1: 1 })) 63 | , Xstream.from(new Id({ count2: 2 }))) 64 | .toFantasyX() 65 | .toStream(intent) 66 | .reduce((acc, f: any) => f(acc), { count: 0 }) 67 | expect(res).toEqual({ "count": 0, "count1": 1, "count2": 2 }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/__tests__/xtest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount, configure } from 'enzyme'; 3 | import '@reactivex/rxjs' 4 | import { X, x } from '../index'; 5 | import * as createClass from 'create-react-class' 6 | import * as rx from '../xs/rx' 7 | const compose = (f, g) => x => f(g(x)); 8 | import { Stream } from '../xs' 9 | import * as Adapter from 'enzyme-adapter-react-16'; 10 | 11 | configure({ adapter: new Adapter() }); 12 | 13 | const CounterView: React.SFC = props => ( 14 |
    15 | {props.count} 16 |
    17 | ) 18 | 19 | CounterView.defaultProps = { count: 0, overwritedProps: 'inner' } 20 | interface Intent { 21 | type: string 22 | value?: any 23 | } 24 | 25 | const xcountable = x((intent$) => { 26 | return { 27 | update$: intent$.map((intent) => { 28 | switch (intent.type) { 29 | case 'inc': 30 | return state => ({ count: state.count + 1 }) 31 | case 'dec': 32 | intent$.next({ type: 'changeDefaultProps', value: -3 }) 33 | intent$.next({ type: 'abs' }) 34 | return state => ({ count: state.count - 1 }) 35 | case 'abs': 36 | return state => ({ count: Math.abs(state.count) }) 37 | case 'changeWrapperProps': 38 | return state => ({ 39 | wrapperProps: intent.value, 40 | overwritedProps: intent.value 41 | }) 42 | case 'changeDefaultProps': 43 | return state => ({ count: intent.value }) 44 | case 'exception': 45 | throw new Error('exception!!!') 46 | default: 47 | return state => state 48 | } 49 | }), 50 | actions: { 51 | inc: () => ({ type: 'inc' }), 52 | dec: () => ({ type: 'dec' }), 53 | exception: () => ({ type: 'exception' }), 54 | changeWrapperProps: (value) => ({ type: 'changeWrapperProps', value }), 55 | changeDefaultProps: (value) => ({ type: 'changeDefaultProps', value }), 56 | } 57 | } 58 | }) 59 | 60 | let Counter = xcountable(CounterView); 61 | const Xs = ['most', 'rx'] 62 | for (let name of Xs) { 63 | describe('X=' + name, () => { 64 | let engine, Xtest, mountx 65 | beforeEach(() => { 66 | engine = require(`../xs/${name}`) 67 | Xtest = require(`../xtests/`)[name] 68 | mountx = compose(mount, y => React.createFactory(X)({}, y)) 69 | }) 70 | describe('actions', () => { 71 | let counterWrapper, counter, t, counterView, actions 72 | beforeEach(() => { 73 | counterWrapper = mountx() 74 | counter = counterWrapper.find(Counter).instance() 75 | counterView = counterWrapper.find(CounterView) 76 | actions = counterView.prop('actions') 77 | t = new Xtest(counterView.props()); 78 | }) 79 | it.only('add intent to intent$ and go through sink$', () => { 80 | return t 81 | .do([ 82 | actions.inc, 83 | actions.inc, 84 | actions.inc, 85 | ]) 86 | .collect(counter) 87 | .then(x => expect(x.count).toBe(3)) 88 | }) 89 | 90 | it('async action', () => { 91 | return t 92 | .do([ 93 | actions.inc, 94 | () => actions.fromEvent({ type: 'inc' }), 95 | () => actions.fromPromise(Promise.resolve({ type: 'inc' })), 96 | ]) 97 | .collect(counter) 98 | .then(state => expect(state.count).toBe(3)) 99 | }) 100 | 101 | it('update can also generate new intent', () => { 102 | return t 103 | .do([ 104 | counter.machine.actions.dec, 105 | ]).collect(counter) 106 | .then(state => expect(state.count).toBe(2)) 107 | }) 108 | }); 109 | 110 | describe('props', () => { 111 | let counterWrapper, counter, t, counterView, actions 112 | beforeEach(() => { 113 | counterWrapper = mountx( 114 | 115 | ) 116 | counter = counterWrapper.find(Counter).getNode() 117 | counterView = counterWrapper.find(CounterView) 118 | actions = counterView.prop('actions') 119 | t = new Xtest(counterView.props()); 120 | }) 121 | it('wrappers props will overwirte components default props', () => { 122 | return t 123 | .do([ 124 | counter.machine.actions.inc, 125 | ]).collect(counter) 126 | .then(state => expect(state.count).toBe(10)) 127 | }) 128 | }) 129 | 130 | describe('scope', () => { 131 | let counterWrapper, counter, t, counterView, actions 132 | beforeEach(() => { 133 | counterWrapper = mountx( 134 | 135 | ) 136 | counter = counterWrapper.find(Counter) 137 | counterView = counterWrapper.find(CounterView) 138 | actions = counterView.prop('actions') 139 | t = new Xtest(counterView.props()); 140 | }) 141 | it('like scope in function, outter component change overwrited props wont works', () => { 142 | return t.plan(1) 143 | .do([ 144 | () => actions.changeDefaultProps(19), 145 | () => counterWrapper.setProps({ wrapperProps: 1, overwritedProps: 1, count: 9 }), 146 | ]).collect(counter.getNode()) 147 | .then(() => expect(counterView.prop('wrapperProps')).toBe(0)) 148 | .then(() => expect(counterView.prop('overwritedProps')).toBe('inner')) 149 | .then(() => expect(counterView.prop('count')).toBe(19)) 150 | }) 151 | }) 152 | 153 | describe('composable', () => { 154 | let counterWrapper, counter, t, counterView, actions 155 | const xxcountable = x((intent$) => { 156 | return { 157 | update$: intent$.map(intent => { 158 | switch (intent.type) { 159 | case 'inc2': 160 | return state => ({ count: state.count + 2 }) 161 | case 'dec2': 162 | return state => ({ count: state.count - 2 }) 163 | default: 164 | return state => state 165 | } 166 | }), 167 | actions: { 168 | inc2: () => ({ type: 'inc2' }), 169 | dec2: () => ({ type: 'dec2' }), 170 | } 171 | } 172 | }) 173 | let xxxcoutable = compose(xxcountable, xcountable) 174 | beforeEach(() => { 175 | Counter = xxxcoutable(CounterView) 176 | counterWrapper = mountx( 177 | 178 | ) 179 | counter = counterWrapper.find(Counter).getNode() 180 | counterView = counterWrapper.find(CounterView) 181 | actions = counterView.prop('actions') 182 | t = new Xtest(counterView.props()); 183 | }) 184 | it('compose xxcountable will provide actions inc2, dec2', () => { 185 | return t 186 | .do([ 187 | actions.inc, 188 | actions.inc2, 189 | actions.dec2, 190 | ]).collect(counter) 191 | .then(state => expect(state.count).toBe(1)) 192 | }) 193 | }) 194 | 195 | describe('ERROR', () => { 196 | let counterWrapper, counter, t, counterView, actions 197 | beforeEach(() => { 198 | spyOn(console, 'error') 199 | counterWrapper = mountx( 200 | 201 | ) 202 | counter = counterWrapper.find(Counter).getNode() 203 | counterView = counterWrapper.find(CounterView) 204 | actions = counterView.prop('actions') 205 | t = new Xtest(counterView.props()); 206 | }) 207 | 208 | it('should recover to identity stream and log exception', () => { 209 | return t 210 | .do([ 211 | actions.inc, 212 | actions.exception, 213 | actions.inc, 214 | ]).collect(counter) 215 | .then(state => expect(state.count).toBe(2)) 216 | .then(() => expect(console.error).toBeCalled) 217 | 218 | }) 219 | }) 220 | 221 | describe('unsubscribe when component unmounted', () => { 222 | it('unsubscribe', (done) => { 223 | const Counter = x((intent$) => { 224 | return { 225 | update$: intent$.map(intent => { 226 | switch (intent.type) { 227 | case 'inc': 228 | return state => ({ count: state.count + 1 }) 229 | default: 230 | return state => state 231 | } 232 | }), 233 | actions: { 234 | inc: () => ({ type: 'inc' }) 235 | } 236 | } 237 | })(CounterView) 238 | 239 | const TogglableMount = createClass({ 240 | getInitialState() { 241 | return { 242 | mount: true 243 | } 244 | }, 245 | render() { 246 | return this.state.mount && 247 | } 248 | }) 249 | spyOn(console, 'error') 250 | let counterWrapper = mountx( 251 | 252 | ) 253 | let toggle = counterWrapper.find(TogglableMount).getNode() 254 | let counter = counterWrapper.find(Counter).getNode() 255 | setTimeout(() => done(), 3) 256 | new Xtest() 257 | .do([ 258 | () => toggle.setState({ mount: false }), 259 | counter.machine.actions.inc, 260 | counter.machine.actions.inc, 261 | ]).collect(counter) 262 | .then(done.fail) 263 | 264 | }) 265 | }) 266 | }) 267 | } 268 | -------------------------------------------------------------------------------- /src/fantasy/fantasyx.ts: -------------------------------------------------------------------------------- 1 | import { Stream, streamOps, Subject } from '../xs' 2 | import { PlanS } from './interfaces' 3 | import { x } from '../x' 4 | import { State } from './state' 5 | import { Actions, Plan, XcomponentClass, Update } from '../interfaces' 6 | import { $ } from './typeclasses' 7 | import { map, Functor } from './typeclasses/functor' 8 | import { FlatMap, flatMap } from './typeclasses/flatmap' 9 | import { Monad } from './typeclasses/monad' 10 | import { Cartesian, product } from './typeclasses/cartesian' 11 | import { Apply } from './typeclasses/apply' 12 | import { Applicative } from './typeclasses/applicative' 13 | import { Traversable } from './typeclasses/traversable' 14 | import { datatype } from './typeclasses' 15 | import { Xstream } from './xstream' 16 | 17 | import * as React from 'react' 18 | 19 | @datatype('FantasyX') 20 | export class FantasyX { 21 | plan: State, $>> 22 | constructor(plan: State, $>>) { 23 | this.plan = plan 24 | } 25 | 26 | apply(WrappedComponent: XcomponentClass | React.ComponentClass | React.SFC, actions?: Actions) { 27 | return x((intent$: Subject) => { 28 | return { update$: this.toStream(intent$), actions } 29 | })(WrappedComponent) 30 | } 31 | 32 | toStream(intent$: Subject): $> { 33 | return streamOps.map, Update>( 34 | s => (state => s.patch(a => a).runS(state)), 35 | this.plan.runA(intent$)) 36 | } 37 | 38 | map(f: (a: A) => B): FantasyX { 39 | return new FantasyX( 40 | Functor.State.map(update$ => ( 41 | streamOps.map, State>(state => ( 42 | Functor.State.map(f, state) 43 | ), update$) 44 | ), this.plan) 45 | ) 46 | } 47 | 48 | foldS(f: (s: S, a: A) => S): FantasyX> { 49 | return new FantasyX>( 50 | Functor.State.map(update$ => ( 51 | streamOps.map, State>>(state => ( 52 | state.patch((a: A, s: S) => f(s, a)) 53 | ), update$) 54 | ), this.plan) 55 | ) 56 | } 57 | 58 | combine( 59 | f: (a: A, b: B) => C, 60 | fB: FantasyX 61 | ): FantasyX { 62 | return new FantasyX( 63 | Monad.State.flatMap(updateA$ => ( 64 | Functor.State.map(updateB$ => ( 65 | streamOps.combine, State, State>((S1, S2) => ( 66 | Monad.State.flatMap(s1 => ( 67 | Functor.State.map(s2 => ( 68 | f(s1, s2) 69 | ), S2) 70 | ), S1) 71 | ), updateA$, updateB$) 72 | ), fB.plan) 73 | ), this.plan)) 74 | } 75 | 76 | merge( 77 | fB: FantasyX 78 | ): FantasyX { 79 | return new FantasyX( 80 | Monad.State.flatMap(updateA$ => ( 81 | Functor.State.map(updateB$ => ( 82 | streamOps.merge, State>(updateA$, updateB$) 83 | ), fB.plan) 84 | ), this.plan)) 85 | } 86 | } 87 | 88 | declare module './typeclasses' { 89 | export interface _ { 90 | "FantasyX": FantasyX 91 | } 92 | } 93 | 94 | export class FantasyXFunctor implements Functor<"FantasyX"> { 95 | map(f: (a: A) => B, fa: FantasyX): FantasyX { 96 | return fa.map(f) 97 | } 98 | } 99 | 100 | declare module './typeclasses/functor' { 101 | export namespace Functor { 102 | export let FantasyX: FantasyXFunctor 103 | } 104 | } 105 | 106 | Functor.FantasyX = new FantasyXFunctor 107 | 108 | export class FantasyXCartesian implements Cartesian<"FantasyX"> { 109 | product(fa: FantasyX, fb: FantasyX): FantasyX { 110 | return new FantasyX( 111 | FlatMap.State.flatMap(s1$ => ( 112 | Functor.State.map(s2$ => ( 113 | streamOps.combine((a: any, b: any) => Cartesian.State.product(a, b), s1$, s2$) 114 | ), fb.plan) 115 | ), fa.plan)) 116 | } 117 | } 118 | 119 | declare module './typeclasses/cartesian' { 120 | export namespace Cartesian { 121 | export let FantasyX: FantasyXCartesian 122 | } 123 | } 124 | 125 | Cartesian.FantasyX = new FantasyXCartesian 126 | 127 | export class FantasyXApply implements Apply<"FantasyX"> { 128 | ap( 129 | fab: FantasyX B>, 130 | fa: FantasyX 131 | ): FantasyX { 132 | return new FantasyX( 133 | FlatMap.State.flatMap(s1$ => ( 134 | Functor.State.map(s2$ => ( 135 | streamOps.combine((s1: any, s2: any) => Apply.State.ap(s1, s2), s1$, s2$) 136 | ), fa.plan) 137 | ), fab.plan)) 138 | } 139 | map = Functor.FantasyX.map 140 | product = Cartesian.FantasyX.product 141 | } 142 | 143 | declare module './typeclasses/apply' { 144 | export namespace Apply { 145 | export let FantasyX: FantasyXApply 146 | } 147 | } 148 | 149 | Apply.FantasyX = new FantasyXApply 150 | 151 | export class FantasyXApplicative extends FantasyXApply { 152 | pure(v: A): FantasyX { 153 | return Applicative.Xstream.pure(v).toFantasyX() 154 | } 155 | } 156 | 157 | declare module './typeclasses/applicative' { 158 | export namespace Applicative { 159 | export let FantasyX: FantasyXApplicative 160 | } 161 | } 162 | 163 | Applicative.FantasyX = new FantasyXApplicative 164 | -------------------------------------------------------------------------------- /src/fantasy/index.ts: -------------------------------------------------------------------------------- 1 | export { Xstream } from './xstream' 2 | export { FantasyX } from './fantasyx' 3 | 4 | export * from './typeclasses/applicative' 5 | export * from './typeclasses/functor' 6 | export * from './typeclasses/apply' 7 | export * from './typeclasses/monad' 8 | export * from './typeclasses/flatmap' 9 | export * from './typeclasses/cartesian' 10 | export * from './typeclasses/semigroup' 11 | export * from './typeclasses/traversable' 12 | -------------------------------------------------------------------------------- /src/fantasy/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Actions } from '../interfaces' 2 | import { State } from './state' 3 | import { Subject, Stream, $ } from '../xs' 4 | export type Partial = { 5 | [P in keyof T]?: T[P]; 6 | } 7 | 8 | export interface pair { 9 | s: S 10 | a: A 11 | } 12 | 13 | export type StateP = State> 14 | 15 | export interface Machine { 16 | actions?: Actions 17 | update$: $> 18 | } 19 | 20 | export type PlanS = (i: Subject) => Machine 21 | -------------------------------------------------------------------------------- /src/fantasy/state.ts: -------------------------------------------------------------------------------- 1 | import { pair, Partial } from './interfaces' 2 | import { Functor } from './typeclasses/functor' 3 | import { Cartesian } from './typeclasses/cartesian' 4 | import { Apply } from './typeclasses/apply' 5 | import { FlatMap } from './typeclasses/flatmap' 6 | import { Applicative } from './typeclasses/applicative' 7 | import { Semigroup, concat } from './typeclasses/semigroup' 8 | import { Monad, MonadInstances } from './typeclasses/monad' 9 | import { datatype } from './typeclasses' 10 | 11 | export const kind = "State" 12 | export type kind = typeof kind 13 | 14 | declare module './typeclasses' { 15 | interface _ { 16 | 'State': State 17 | } 18 | } 19 | 20 | @datatype(kind) 21 | export class State { 22 | runState: (s: S) => pair 23 | constructor(runState: (s: S) => pair) { 24 | this.runState = runState 25 | } 26 | 27 | runA(state: S): A { 28 | return this.runState(state).a 29 | } 30 | 31 | runS(state: S): S { 32 | return this.runState(state).s 33 | } 34 | 35 | static pure(a: A): State { 36 | return new State((s: S) => ({ a, s })) 37 | } 38 | 39 | static get(): State { 40 | return new State((s: S) => ({ s: s, a: s })) 41 | } 42 | 43 | static put(s: S): State { 44 | return new State((_: S) => ({ a: undefined, s: s })) 45 | } 46 | 47 | static modify(f: (s: S) => Partial): State { 48 | return new State((s: S) => ({ a: undefined, s: Object.assign({}, s, f(s)) })) 49 | } 50 | 51 | patch(f: (a: A, s: S) => Partial): State> { 52 | return new State((state: S) => { 53 | let { a, s } = this.runState(state) 54 | let p = f(a, s) 55 | return { 56 | a: p, s: Object.assign({}, s, p) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | export class StateFunctor implements Functor { 63 | map(f: (a: A) => B, fa: State): State { 64 | return new State((state: C) => { 65 | let { a, s } = fa.runState(state) 66 | return { a: f(a), s: s } 67 | }) 68 | } 69 | } 70 | 71 | declare module './typeclasses/functor' { 72 | namespace Functor { 73 | let State: StateFunctor 74 | } 75 | } 76 | 77 | Functor.State = new StateFunctor 78 | 79 | export class StateCartesian implements Cartesian { 80 | product(fa: State, fb: State): State { 81 | return new State((state: S) => { 82 | let { a: a1, s: s1 } = fa.runState(state) 83 | let { a: a2, s: s2 } = fb.runState(s1) 84 | return { a: [a1, a2] as [A, B], s: s2 } 85 | }) 86 | } 87 | } 88 | 89 | declare module './typeclasses/cartesian' { 90 | namespace Cartesian { 91 | let State: StateCartesian 92 | } 93 | } 94 | 95 | Cartesian.State = new StateCartesian 96 | 97 | export class StateApply implements Apply { 98 | ap(fab: State B>, fa: State): State { 99 | return new State((state: S) => { 100 | let { a: f, s: s1 } = fab.runState(state) 101 | let { a, s: s2 } = fa.runState(s1) 102 | return { a: f(a), s: s2 } 103 | }) 104 | } 105 | map = Functor.State.map 106 | product = Cartesian.State.product 107 | } 108 | 109 | declare module './typeclasses/apply' { 110 | namespace Apply { 111 | let State: StateApply 112 | } 113 | } 114 | Apply.State = new StateApply 115 | 116 | export class StateApplicative extends StateApply implements Applicative { 117 | pure = State.pure 118 | } 119 | 120 | declare module './typeclasses/applicative' { 121 | namespace Applicative { 122 | let State: StateApplicative 123 | } 124 | } 125 | Applicative.State = new StateApplicative 126 | 127 | export class StateFlatMap extends StateApplicative implements FlatMap { 128 | flatMap(f: (a: A) => State, fa: State): State { 129 | return new State((state: S) => { 130 | let { a, s } = fa.runState(state) 131 | return f(a).runState(s) 132 | }) 133 | } 134 | } 135 | 136 | declare module './typeclasses/flatmap' { 137 | namespace FlatMap { 138 | let State: StateFlatMap 139 | } 140 | } 141 | 142 | FlatMap.State = new StateFlatMap 143 | 144 | export class StateMonad extends StateApplicative implements Monad { 145 | flatMap = FlatMap.State.flatMap 146 | } 147 | 148 | declare module './typeclasses/monad' { 149 | namespace Monad { 150 | let State: StateMonad 151 | } 152 | } 153 | 154 | Monad.State = new StateMonad 155 | -------------------------------------------------------------------------------- /src/fantasy/streamT.ts: -------------------------------------------------------------------------------- 1 | import { Stream, streamOps } from '../xs' 2 | import { $ } from './typeclasses' 3 | import { FunctorInstances, map } from './typeclasses/functor' 4 | 5 | 6 | class StreamT { 7 | value: $> 8 | constructor(v: $>) { 9 | this.value = v 10 | } 11 | map(f: (a: A) => B): StreamT { 12 | return new StreamT( 13 | map((s: $) => streamOps.map(f, s), this.value) 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/applicative.ts: -------------------------------------------------------------------------------- 1 | import { Apply, ap2 } from './apply' 2 | import { $, HKT, kind } from '.' 3 | 4 | export type ApplicativeInstances = keyof typeof Applicative 5 | export interface Applicative extends Apply { 6 | pure(v: A): $ 7 | } 8 | export namespace Applicative { 9 | const __name = "Applicative" 10 | } 11 | 12 | export function lift2(fabc: (a: A, b: B) => C): (fa: $, fb: $) => $ { 13 | return function(fa: $, fb: $): $ { 14 | let instance = Applicative[kind(fa)] as Applicative 15 | return ap2(instance.pure(fabc), fa, fb) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/apply.ts: -------------------------------------------------------------------------------- 1 | import { Functor } from './functor' 2 | import { Cartesian } from './cartesian' 3 | import { $, HKT, kind } from '.' 4 | 5 | export interface Apply extends Cartesian, Functor { 6 | ap(fab: $ B>, fa: $): $ 7 | } 8 | 9 | export type ApplyInstances = keyof typeof Apply 10 | 11 | export namespace Apply { 12 | const __name = "Apply" 13 | } 14 | 15 | export function ap(fab: $ B>, fa: $): $ { 16 | let instance = Apply[kind(fab)] as Apply 17 | let faba = instance.product<(a: A) => B, A>(fab, fa) 18 | return instance.map((aba: [(a: A) => B, A]) => aba[0](aba[1]), faba) 19 | } 20 | 21 | export function ap2(fabc: $ C>, fa: $, fb: $): $ { 22 | let instance = Apply[kind(fabc)] as Apply 23 | return ap( 24 | instance.map( 25 | (f: (a: A, b: B) => C) => (([a, b]: [A, B]) => f(a, b)) 26 | , fabc) 27 | , instance.product(fa, fb) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/cartesian.ts: -------------------------------------------------------------------------------- 1 | import { $, HKT, kind } from '.' 2 | 3 | export type CartesianInstances = keyof typeof Cartesian 4 | export interface Cartesian { 5 | product(fa: $, fb: $): $ 6 | } 7 | 8 | export namespace Cartesian { 9 | const __name = "Cartesian" 10 | } 11 | 12 | export function product(fa: $, fb: $): $ { 13 | let instance = Cartesian[kind(fa)] as Cartesian 14 | return instance.product(fa, fb) 15 | } 16 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/flatmap.ts: -------------------------------------------------------------------------------- 1 | import { Apply } from './apply' 2 | import { $, HKT, kind } from '.' 3 | 4 | export interface FlatMap extends Apply { 5 | flatMap(f: (a: A) => $, fb: $): $ 6 | } 7 | export type FlatMapInstances = keyof typeof FlatMap 8 | export namespace FlatMap { 9 | const __name = "FlatMap" 10 | } 11 | 12 | export function flatMap(f: (a: A) => $, fa: $): $ { 13 | return (FlatMap[kind(fa)] as FlatMap).flatMap(f, fa) 14 | } 15 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/functor.ts: -------------------------------------------------------------------------------- 1 | import { $, HKT, datatype, kind } from '.' 2 | export interface Functor { 3 | map(f: (a: A) => B, fa: $): $ 4 | } 5 | 6 | export type FunctorInstances = keyof typeof Functor 7 | 8 | export namespace Functor { 9 | export let Array = { 10 | map: (f: (a: A) => B, fb: A[]) => fb.map(f) 11 | } 12 | } 13 | 14 | export function map(f: (a: A) => B, fa: $): $ { 15 | let instance = Functor[kind(fa)] as Functor 16 | return instance.map(f, fa) 17 | } 18 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/id.ts: -------------------------------------------------------------------------------- 1 | import { datatype, $ } from '.' 2 | import { Functor } from './functor' 3 | import { ap, Apply } from './apply' 4 | import { Cartesian } from './cartesian' 5 | import { Applicative } from './applicative' 6 | import { FlatMap } from './flatmap' 7 | import { Monad } from './monad' 8 | 9 | @datatype('Id') 10 | export class Id { 11 | value: A 12 | constructor(value: A) { 13 | this.value = value 14 | } 15 | valueOf() { 16 | return this.value 17 | } 18 | } 19 | 20 | export class IdFunctor implements Functor<"Id"> { 21 | map(f: (a: A) => B, fb: $<"Id", A>): $<"Id", B> { 22 | return new Id(f(fb.value)) 23 | } 24 | } 25 | 26 | declare module '.' { 27 | interface _ { 28 | "Id": Id 29 | } 30 | } 31 | 32 | declare module './functor' { 33 | namespace Functor { 34 | export let Id: IdFunctor 35 | } 36 | } 37 | 38 | Functor.Id = new IdFunctor 39 | 40 | export class IdCartesian implements Cartesian<"Id"> { 41 | product(fa: Id, fb: Id): Id<[A, B]> { 42 | return new Id([fa.value, fb.value] as [A, B]) 43 | } 44 | } 45 | 46 | declare module './cartesian' { 47 | namespace Cartesian { 48 | export let Id: IdCartesian 49 | } 50 | } 51 | 52 | Cartesian.Id = new IdCartesian 53 | 54 | export class IdApply implements Apply<"Id"> { 55 | ap(fab: Id<(a: A) => B>, fa: Id): Id { 56 | return ap<"Id", A, B>(fab, fa) 57 | } 58 | map = Functor.Id.map 59 | product = Cartesian.Id.product 60 | } 61 | 62 | declare module './apply' { 63 | namespace Apply { 64 | export let Id: IdApply 65 | } 66 | } 67 | 68 | Apply.Id = new IdApply 69 | 70 | export class IdApplicative extends IdApply implements Applicative<"Id">{ 71 | pure(a: A): Id { 72 | return new Id(a) 73 | } 74 | } 75 | 76 | declare module './applicative' { 77 | namespace Applicative { 78 | export let Id: IdApplicative 79 | } 80 | } 81 | 82 | Applicative.Id = new IdApplicative 83 | 84 | 85 | export class IdFlatMap extends IdApply implements FlatMap<"Id">{ 86 | flatMap(f: (a: A) => Id, fa: Id): Id { 87 | return this.map(f, fa).value 88 | } 89 | } 90 | 91 | declare module './flatmap' { 92 | namespace FlatMap { 93 | export let Id: IdFlatMap 94 | } 95 | } 96 | 97 | FlatMap.Id = new IdFlatMap 98 | 99 | 100 | export class IdMonad extends IdApplicative implements Monad<"Id">{ 101 | flatMap = FlatMap.Id.flatMap 102 | } 103 | 104 | declare module './monad' { 105 | namespace Monad { 106 | export let Id: IdMonad 107 | } 108 | } 109 | 110 | Monad.Id = new IdMonad 111 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/index.ts: -------------------------------------------------------------------------------- 1 | export interface _ { 2 | "Array": Array 3 | } 4 | 5 | export type HKT = keyof _ 6 | 7 | export type $ = _[F] 8 | 9 | import 'reflect-metadata' 10 | 11 | export function datatype(name: string) { 12 | return (constructor: Function) => { 13 | Reflect.defineMetadata('design:type', name, constructor); 14 | } 15 | } 16 | 17 | export function datatypeOf(target: any): string { 18 | if (isPrimitive(target)) { 19 | return target.constructor.name 20 | } 21 | else { 22 | let tag = Reflect.getMetadata('design:type', target.constructor); 23 | if (tag) return tag 24 | throw new Error(`target ${target.constructor} is not a datatype, please decorate it with @datatype!`) 25 | } 26 | } 27 | 28 | export function kind(target: $): F { 29 | return datatypeOf(target) as F 30 | } 31 | 32 | datatype('Array')(Array) 33 | datatype('Object')(Object) 34 | datatype('Promise')(Promise) 35 | 36 | 37 | function isPrimitive(a: any): boolean { 38 | return ['string', 'number', 'symbol', 'boolean'].indexOf(typeof a) >= 0 39 | } 40 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/monad.ts: -------------------------------------------------------------------------------- 1 | import { Apply } from './apply' 2 | import { FlatMap } from './flatmap' 3 | import { $, HKT } from '.' 4 | 5 | export type MonadInstances = keyof typeof Monad 6 | 7 | export interface Monad extends FlatMap, Apply { } 8 | 9 | export namespace Monad { 10 | const __name = "Monad" 11 | } 12 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/semigroup.ts: -------------------------------------------------------------------------------- 1 | export type instanceKey = keyof S 2 | export type S = typeof Semigroup 3 | import { datatype, datatypeOf } from '.' 4 | export type SemigroupInstance = keyof typeof Semigroup 5 | export type SemigroupInstanceType = typeof Semigroup[SemigroupInstance]['_T'] 6 | export interface Semigroup { 7 | _T: A 8 | concat(a: any, b: any): A 9 | } 10 | 11 | export class NumberSemigroup implements Semigroup { 12 | _T: number 13 | concat(a: any, b: any): number { 14 | return a + b 15 | } 16 | } 17 | 18 | export class ObjectSemigroup implements Semigroup { 19 | _T: object 20 | concat(a: any, b: any): object { 21 | return Object.assign({}, a, b) 22 | } 23 | } 24 | 25 | export class PromiseSemigroup implements Semigroup> { 26 | _T: Promise 27 | concat(a: any, b: any): Promise { 28 | return Promise.all([a, b]).then(([a, b]) => concat(a, b)) 29 | } 30 | } 31 | export class StringSemigroup implements Semigroup { 32 | _T: string 33 | concat(a: any, b: any): string { 34 | return a + b 35 | } 36 | } 37 | 38 | export class ArraySemigroup implements Semigroup> { 39 | _T: Array 40 | concat(a: Array, b: Array): Array { 41 | return a.concat(b) 42 | } 43 | } 44 | 45 | export namespace Semigroup { 46 | export let Number = new NumberSemigroup 47 | export let String = new StringSemigroup 48 | export let Array = new ArraySemigroup() 49 | export let Object = new ObjectSemigroup 50 | export let Promise = new PromiseSemigroup 51 | } 52 | 53 | export function concat(a: A, b: A): A { 54 | let instance = (Semigroup)[datatypeOf(a)] 55 | return instance.concat(a, b) 56 | } 57 | -------------------------------------------------------------------------------- /src/fantasy/typeclasses/traversable.ts: -------------------------------------------------------------------------------- 1 | import { $, HKT, kind } from '.' 2 | import { Functor, map } from './functor' 3 | import { ApplicativeInstances, Applicative, lift2 } from './applicative' 4 | 5 | export interface Traversable extends Functor { 6 | traverse(f: (a: A) => $, ta: $): $> 7 | } 8 | 9 | export type TraversableInstances = keyof typeof Traversable 10 | 11 | export class TraversableArray { 12 | traverse(name: F): (f: (a: A, index?: number) => $, ta: A[]) => $ { 13 | return (f: (a: A, index?: number) => $, ta: A[]): $ => { 14 | let instance = Applicative[name] 15 | return ta.reduce<$>( 16 | (acc, i, index) => lift2((a: B[], b: B[]) => a.concat(b))( 17 | acc 18 | , map(a => [a], f(i, index))) 19 | , instance.pure([]) as $ 20 | ) 21 | } 22 | } 23 | } 24 | 25 | export namespace Traversable { 26 | export let Array = new TraversableArray 27 | } 28 | -------------------------------------------------------------------------------- /src/fantasy/xstream.ts: -------------------------------------------------------------------------------- 1 | import { Functor, FunctorInstances, map } from './typeclasses/functor' 2 | import { Cartesian, product } from './typeclasses/cartesian' 3 | import { Apply } from './typeclasses/apply' 4 | import { FlatMap, flatMap } from './typeclasses/flatmap' 5 | import { Applicative } from './typeclasses/applicative' 6 | import { Semigroup, concat, SemigroupInstanceType } from './typeclasses/semigroup' 7 | import { Monad } from './typeclasses/monad' 8 | import { streamOps, Subject, Stream } from '../xs' 9 | import { Plan, Update } from '../interfaces' 10 | import { FantasyX } from './fantasyx' 11 | import { State } from './state' 12 | import { datatype, $, HKT } from './typeclasses' 13 | 14 | @datatype('Xstream') 15 | export class Xstream { 16 | streamS: State<$, $> 17 | constructor(streamS: State<$, $>) { 18 | this.streamS = streamS 19 | } 20 | 21 | filter(f: (a: A) => boolean): Xstream { 22 | return new Xstream(Monad.State.map(sa => streamOps.filter(f, sa), this.streamS)) 23 | } 24 | 25 | static fromIntent() { 26 | return new Xstream(new State((intent$: $) => ({ 27 | s: intent$, 28 | a: intent$ 29 | }))) 30 | } 31 | 32 | static fromEvent(type: string, name: string, defaultValue?: string) { 33 | return new Xstream(new State((intent$: $) => ({ 34 | s: intent$, 35 | a: streamOps.merge( 36 | typeof defaultValue != 'undefined' ? streamOps.just(defaultValue) : streamOps.empty() 37 | , 38 | streamOps.map((e: Event) => (e.target as HTMLFormElement).value 39 | , streamOps.filter((e: Event) => { 40 | let target = e.target as HTMLFormElement 41 | return target.tagName == 'INPUT' && e.type == type && target.name == name 42 | }, (intent$ as $))) 43 | 44 | ) 45 | }))) 46 | } 47 | 48 | static fromPromise(p: Promise) { 49 | return new Xstream(new State((intent$: $) => ({ 50 | s: intent$, 51 | a: streamOps.fromPromise(p) 52 | }))) 53 | } 54 | 55 | static from(p: $) { 56 | return new Xstream(new State((intent$: $) => ({ 57 | s: intent$, 58 | a: streamOps.from(p) as $ 59 | }))) 60 | } 61 | 62 | toFantasyX() { 63 | type itentStream = Subject 64 | type updateStream = $> 65 | return new FantasyX( 66 | new State(intent$ => { 67 | let state$ = this.streamS.runA(intent$) 68 | return { 69 | s: intent$, 70 | a: streamOps.map>((a: A) => Applicative.State.pure(a), state$) 71 | } 72 | })) 73 | } 74 | } 75 | 76 | declare module './typeclasses' { 77 | export interface _ { 78 | "Xstream": Xstream 79 | } 80 | } 81 | 82 | declare module './typeclasses/functor' { 83 | export namespace Functor { 84 | export let Xstream: XstreamFunctor 85 | } 86 | } 87 | 88 | export class XstreamFunctor implements Functor<"Xstream">{ 89 | map(f: (a: A) => B, fa: Xstream): Xstream { 90 | return new Xstream(Monad.State.map(sa => streamOps.map(f, sa), fa.streamS)) 91 | } 92 | } 93 | 94 | Functor.Xstream = new XstreamFunctor 95 | 96 | export class XstreamCartesian implements Cartesian<"Xstream">{ 97 | product(fa: Xstream, fb: Xstream): Xstream { 98 | return new Xstream( 99 | FlatMap.State.flatMap(s1 => ( 100 | Functor.State.map(s2 => ( 101 | streamOps.combine((a, b) => [a, b], s1, s2) 102 | ), fb.streamS) 103 | ), fa.streamS)) 104 | } 105 | } 106 | 107 | Cartesian.Xstream = new XstreamCartesian 108 | 109 | declare module './typeclasses/cartesian' { 110 | export namespace Cartesian { 111 | export let Xstream: XstreamCartesian 112 | } 113 | } 114 | 115 | export class XstreamApply implements Apply<"Xstream"> { 116 | ap( 117 | fab: Xstream B>, 118 | fa: Xstream 119 | ): Xstream { 120 | return new Xstream( 121 | FlatMap.State.flatMap(s1 => ( 122 | Functor.State.map(s2 => ( 123 | streamOps.combine<(a: A) => B, A, B>((a, b) => a(b), s1, s2) 124 | ), fa.streamS) 125 | ), fab.streamS)) 126 | } 127 | map = Functor.Xstream.map 128 | product = Cartesian.Xstream.product 129 | } 130 | 131 | Apply.Xstream = new XstreamApply 132 | 133 | declare module './typeclasses/apply' { 134 | export namespace Apply { 135 | export let Xstream: XstreamApply 136 | } 137 | } 138 | 139 | export class XstreamFlatMap extends XstreamApply { 140 | flatMap(f: (a: A) => Xstream, fa: Xstream): Xstream { 141 | return new Xstream( 142 | FlatMap.State.flatMap((a$: $) => ( 143 | map<"State", $, $>(i$ => { 144 | let sdf = (a: A) => f(a).streamS.runA(i$) 145 | return streamOps.flatMap(sdf, a$) 146 | }, State.get<$>()) 147 | ), fa.streamS) 148 | ) 149 | } 150 | } 151 | 152 | FlatMap.Xstream = new XstreamFlatMap 153 | 154 | declare module './typeclasses/flatmap' { 155 | export namespace FlatMap { 156 | export let Xstream: XstreamFlatMap 157 | } 158 | } 159 | 160 | export class XstreamApplicative extends XstreamApply { 161 | pure(v: A): Xstream { 162 | return new Xstream( 163 | Applicative.State.pure(streamOps.just(v)) 164 | ) 165 | } 166 | } 167 | 168 | Applicative.Xstream = new XstreamApplicative 169 | 170 | declare module './typeclasses/applicative' { 171 | export namespace Applicative { 172 | export let Xstream: XstreamApplicative 173 | } 174 | } 175 | 176 | export class XstreamMonad extends XstreamApplicative implements FlatMap<"Xstream"> { 177 | flatMap = FlatMap.Xstream.flatMap 178 | } 179 | 180 | Monad.Xstream = new XstreamMonad 181 | 182 | declare module './typeclasses/monad' { 183 | export namespace Monad { 184 | export let Xstream: XstreamMonad 185 | } 186 | } 187 | 188 | export class XstreamSemigroup implements Semigroup> { 189 | _T: Xstream 190 | concat(fa: Xstream, fb: Xstream): Xstream { 191 | return Functor.Xstream.map( 192 | ([a, b]) => concat(a, b) 193 | , Cartesian.Xstream.product(fa, fb)) 194 | } 195 | } 196 | 197 | Semigroup.Xstream = new XstreamSemigroup 198 | 199 | declare module './typeclasses/semigroup' { 200 | export namespace Semigroup { 201 | export let Xstream: XstreamSemigroup 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './x' 2 | export * from './interfaces' 3 | export * from './xclass' 4 | export * from './fantasy' 5 | export * from './xs' 6 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Stream, Subject, Subscription } from './xs' 3 | import { $ } from './fantasy/typeclasses' 4 | import * as PropTypes from 'prop-types'; 5 | 6 | export const XREACT_ENGINE = '@reactive-react/xreact.engine'; 7 | 8 | export interface Actions { 9 | [propName: string]: (...v: any[]) => T 10 | } 11 | 12 | export interface Plan { 13 | (intent: Subject, props?: Xprops): Machine 14 | } 15 | 16 | export interface Update { 17 | (current: S): Partial 18 | } 19 | 20 | export interface Machine { 21 | actions?: Actions, 22 | update$: $> 23 | } 24 | 25 | export interface ConfiguredMachine { 26 | actions?: Actions, 27 | update$: $> 28 | } 29 | 30 | export interface Xprops { 31 | actions?: Actions 32 | history?: boolean 33 | [propName: string]: any; 34 | } 35 | 36 | export class Xcomponent extends React.PureComponent, S> { 37 | machine: ConfiguredMachine 38 | subscription: Subscription 39 | context: ContextEngine 40 | } 41 | 42 | export interface XcomponentClass { 43 | displayName: string 44 | contextTypes?: ContextType 45 | defaultProps?: any 46 | new(props: Xprops, context: ContextEngine): Xcomponent; 47 | } 48 | 49 | export interface History { 50 | path: Subject number> 51 | history: $ 52 | } 53 | 54 | export interface Stamp { 55 | value: S 56 | time: number 57 | } 58 | 59 | export interface Engine { 60 | intent$: Subject 61 | history$: Subject 62 | } 63 | 64 | export interface ContextEngine { 65 | [name: string]: Engine 66 | } 67 | 68 | export interface ContextType { 69 | [name: string]: PropTypes.Requireable> 70 | } 71 | -------------------------------------------------------------------------------- /src/x.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types' 3 | 4 | import { extendXComponentClass, genXComponentClass, CONTEXT_TYPE } from './xclass' 5 | import { streamOps, Stream, Subject } from './xs' 6 | import { Plan, Xcomponent, XcomponentClass, Engine, ContextEngine, XREACT_ENGINE } from './interfaces' 7 | export { XREACT_ENGINE } 8 | 9 | export function isXcomponentClass( 10 | ComponentClass: any): ComponentClass is XcomponentClass { 11 | return (ComponentClass).contextTypes == CONTEXT_TYPE; 12 | } 13 | export type XOrReactComponent = XcomponentClass | React.ComponentClass | React.SFC 14 | 15 | export function x(main: Plan, opts = {}): (WrappedComponent: React.ComponentType | XcomponentClass) => XcomponentClass { 16 | return function(WrappedComponent: React.ComponentType | XcomponentClass) { 17 | if (isXcomponentClass(WrappedComponent)) { 18 | return extendXComponentClass(WrappedComponent, main) 19 | } else { 20 | return genXComponentClass(WrappedComponent, main, opts) 21 | } 22 | }; 23 | } 24 | 25 | export class X extends React.PureComponent<{}, {}> { 26 | static childContextTypes = CONTEXT_TYPE 27 | getChildContext(): ContextEngine { 28 | return { 29 | [XREACT_ENGINE]: { 30 | intent$: streamOps.subject() as Subject, 31 | history$: streamOps.subject() as Subject 32 | } 33 | } 34 | } 35 | render() { 36 | return React.Children.only(this.props.children); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/xclass.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createElement as h } from 'react' 3 | import * as PropTypes from 'prop-types'; 4 | import { Plan, Xcomponent, XcomponentClass, ContextEngine, XREACT_ENGINE, Update, Actions, Xprops } from './interfaces' 5 | import { streamOps, Stream, Subject } from './xs' 6 | 7 | export const CONTEXT_TYPE = { 8 | [XREACT_ENGINE]: PropTypes.shape({ 9 | intent$: PropTypes.object, 10 | history$: PropTypes.object 11 | }) 12 | }; 13 | function isSFC(Component: React.ComponentClass | React.SFC): Component is React.SFC { 14 | return (typeof Component == 'function') 15 | } 16 | 17 | export function extendXComponentClass(WrappedComponent: XcomponentClass, main: Plan): XcomponentClass { 18 | return class XNode extends WrappedComponent { 19 | static contextTypes = CONTEXT_TYPE 20 | static displayName = `X(${getDisplayName(WrappedComponent)})` 21 | constructor(props: Xprops, context: ContextEngine) { 22 | super(props, context); 23 | let engine = context[XREACT_ENGINE] 24 | let { actions, update$ } = main(engine.intent$, props) 25 | this.machine.update$ = streamOps.merge, Update>(this.machine.update$, update$) 26 | if (actions) 27 | this.machine.actions = Object.assign({}, bindActions(actions, engine.intent$, this), this.machine.actions) 28 | } 29 | } 30 | } 31 | export function genXComponentClass(WrappedComponent: React.ComponentType, main: Plan, opts?: any): XcomponentClass { 32 | return class XLeaf extends Xcomponent { 33 | static contextTypes = CONTEXT_TYPE 34 | static displayName = `X(${getDisplayName(WrappedComponent)})` 35 | defaultKeys: (keyof S)[] 36 | constructor(props: Xprops, context: ContextEngine) { 37 | super(props, context); 38 | let engine = context[XREACT_ENGINE] 39 | let { actions, update$ } = main(engine.intent$, props) 40 | this.machine = { 41 | update$: update$ 42 | } 43 | this.machine.actions = bindActions(actions || {}, engine.intent$, this) 44 | 45 | this.defaultKeys = WrappedComponent.defaultProps ? (<(keyof S)[]>Object.keys(WrappedComponent.defaultProps)) : []; 46 | this.state = (Object.assign( 47 | {}, 48 | WrappedComponent.defaultProps, 49 | >pick(this.defaultKeys, props) 50 | )); 51 | } 52 | componentWillReceiveProps(nextProps: I) { 53 | this.setState((state, props) => Object.assign({}, nextProps, pick(this.defaultKeys, state))); 54 | } 55 | componentDidMount() { 56 | this.subscription = streamOps.subscribe( 57 | this.machine.update$, 58 | (action: Update) => { 59 | if (action instanceof Function) { 60 | if (process.env.NODE_ENV == 'debug') 61 | console.log('UPDATE:', action) 62 | this.setState((prevState, props) => { 63 | let newState: S = action.call(this, prevState, props); 64 | this.context[XREACT_ENGINE].history$.next(newState) 65 | if (process.env.NODE_ENV == 'debug') 66 | console.log('STATE:', newState) 67 | return newState; 68 | }); 69 | } else { 70 | /* istanbul ignore next */ 71 | console.warn( 72 | 'action', 73 | action, 74 | 'need to be a Function which map from current state to new state' 75 | ); 76 | } 77 | }, 78 | () => { 79 | this.context[XREACT_ENGINE].history$.complete(this.state) 80 | if (process.env.NODE_ENV == 'production') { 81 | console.error('YOU HAVE TERMINATED THE INTENT STREAM...') 82 | } 83 | if (process.env.NODE_ENV == 'debug') { 84 | console.log(`LAST STATE is`, this.state) 85 | } 86 | } 87 | ); 88 | } 89 | componentWillUnmount() { 90 | this.subscription.unsubscribe(); 91 | } 92 | render() { 93 | if (isSFC(WrappedComponent)) { 94 | return h( 95 | WrappedComponent, 96 | Object.assign({}, opts, this.props, this.state, { 97 | actions: this.machine.actions, 98 | }) 99 | ); 100 | } else { 101 | return h( 102 | WrappedComponent, 103 | Object.assign({}, opts, this.props, this.state, { 104 | actions: this.machine.actions, 105 | }) 106 | ); 107 | } 108 | } 109 | } 110 | } 111 | 112 | function getDisplayName(WrappedComponent: React.ComponentType) { 113 | return WrappedComponent.displayName || WrappedComponent.name || 'X'; 114 | } 115 | 116 | 117 | function bindActions(actions: Actions, intent$: Subject, self: XcomponentClass | Xcomponent) { 118 | let _actions: Actions = { 119 | fromEvent(e: Event) { 120 | return intent$.next(e); 121 | }, 122 | fromPromise(p: Promise) { 123 | return p.then(x => intent$.next(x)); 124 | }, 125 | terminate(a: I) { 126 | if (process.env.NODE_ENV == 'debug') 127 | console.error('INTENT TERMINATED') 128 | return intent$.complete(a) 129 | } 130 | }; 131 | 132 | for (let a in actions) { 133 | _actions[a] = (...args: any[]) => { 134 | return intent$.next(actions[a].apply(self, args)); 135 | }; 136 | } 137 | return _actions; 138 | } 139 | function pick(names: Array, obj: A) { 140 | let result = >{}; 141 | for (let name of names) { 142 | if (obj[name]) result[name] = obj[name]; 143 | } 144 | return result; 145 | } 146 | 147 | function isPromise(p: any): p is Promise { 148 | return p !== null && typeof p === 'object' && typeof p.then === 'function' 149 | } 150 | -------------------------------------------------------------------------------- /src/xs/array.ts: -------------------------------------------------------------------------------- 1 | import { Subscription, StreamOps } from './index' 2 | 3 | declare module '.' { 4 | interface S_ { 5 | 'ArrayStream': Array 6 | } 7 | } 8 | 9 | declare module '../fantasy/typeclasses' { 10 | interface _ { 11 | 'ArrayStream': Array 12 | } 13 | } 14 | 15 | StreamOps.prototype.empty = function() { 16 | return [] 17 | } 18 | 19 | StreamOps.prototype.just = function(a) { 20 | return [a] 21 | } 22 | 23 | StreamOps.prototype.scan = function(f, base, fa) { 24 | return fa.scan(f, base) 25 | } 26 | 27 | StreamOps.prototype.combine = function ( 28 | f: (...a: any[]) => C, 29 | ...v: any[] 30 | ): Array { 31 | return f.apply(null, v) 32 | } 33 | 34 | StreamOps.prototype.filter = function (f: (a: A) => boolean, fa: Array): Array { 35 | return fa.filter(f) 36 | } 37 | StreamOps.prototype.map = function (f: (a: A) => B, fa: Array): Array { 38 | return fa.map(f) 39 | } 40 | StreamOps.prototype.flatMap = function (f: (a: A) => Array, fa: Array): Array { 41 | return fa.reduce((acc, a) => acc.concat(f(a)), [] as B[]) 42 | } 43 | 44 | function Subject() { 45 | } 46 | Subject.prototype = Array.prototype 47 | 48 | Subject.prototype.next = function(a: any) { 49 | this.push(a) 50 | } 51 | 52 | Subject.prototype.complete = function() { 53 | } 54 | 55 | StreamOps.prototype.subject = function () { 56 | return new (Subject)() 57 | } 58 | 59 | StreamOps.prototype.subscribe = function (fa: Array, next: (v: A) => void, complete?: () => void) { 60 | throw Error("you don't need to subscribe a Array, just iterate it") 61 | } 62 | 63 | StreamOps.prototype.merge = function (a: Array, b: Array): Array { 64 | return (a).concat(b) 65 | } 66 | 67 | StreamOps.prototype.fromPromise = function(p) { 68 | if (p.then) { 69 | throw Error("You're not using real Promise aren't you, expecting Id Monad") 70 | } 71 | return [p.valueOf()] 72 | } 73 | 74 | StreamOps.prototype.from = function(fa) { 75 | return [fa.valueOf()] 76 | } 77 | -------------------------------------------------------------------------------- /src/xs/index.ts: -------------------------------------------------------------------------------- 1 | import { _, $ } from '../fantasy/typeclasses' 2 | import { FunctorInstances } from '../fantasy/typeclasses/functor' 3 | export interface S_ { } 4 | export type Stream = keyof S_ 5 | 6 | export interface FantasySubject { 7 | next(a: A): void 8 | complete(a?: A): void 9 | } 10 | export interface Subscription { 11 | unsubscribe(): void; 12 | } 13 | 14 | export type Subject = $ & FantasySubject 15 | export type $ = $ 16 | 17 | export class StreamOps { } 18 | export interface StreamOps { 19 | empty(): $ 20 | fromPromise(p: Promise): $ 21 | from(fa: $): $ 22 | just(a: A): $ 23 | merge( 24 | a: $, 25 | b: $ 26 | ): $ 27 | scan( 28 | f: (acc: B, cur: A) => B, 29 | base: B, 30 | fa: $ 31 | ): $ 32 | map(f: (a: A) => B, fa: $): $ 33 | filter(f: (a: A) => boolean, fa: $): $ 34 | flatMap(f: (a: A) => $, fa: $): $ 35 | subject(): Subject 36 | combine( 37 | f: (a: A, b: B) => C, 38 | fa: $, 39 | fb: $ 40 | ): $ 41 | combine( 42 | f: (a: A, b: B, c: C) => D, 43 | fa: $, 44 | fb: $, 45 | fc: $ 46 | ): $ 47 | combine( 48 | f: (a: A, b: B, c: C, d: D) => E, 49 | fa: $, 50 | fb: $, 51 | fc: $, 52 | fd: $ 53 | ): $ 54 | combine( 55 | f: (a: A, b: B, c: C, d: D, e: E) => G, 56 | fa: $, 57 | fb: $, 58 | fc: $, 59 | fd: $, 60 | fe: $ 61 | ): $ 62 | combine( 63 | f: (a: A, b: B, c: C, d: D, e: E, g: G) => H, 64 | fa: $, 65 | fb: $, 66 | fc: $, 67 | fd: $, 68 | fe: $, 69 | fg: $ 70 | ): $ 71 | subscribe(fa: $, next: (v: A) => void, complete?: () => void): Subscription 72 | } 73 | 74 | export const streamOps: StreamOps = new StreamOps 75 | -------------------------------------------------------------------------------- /src/xs/most.ts: -------------------------------------------------------------------------------- 1 | import { Stream as MostStream, empty, just, combineArray, combine, flatMap, fromPromise, from } from 'most' 2 | import { sync, SyncSubject, Subject } from 'most-subject' 3 | import { Subscription, StreamOps } from '.' 4 | 5 | declare module '.' { 6 | interface S_ { 7 | 'MostStream': MostStream 8 | } 9 | } 10 | 11 | 12 | declare module '../fantasy/typeclasses' { 13 | interface _ { 14 | 'MostStream': MostStream 15 | } 16 | } 17 | 18 | StreamOps.prototype.empty = empty 19 | StreamOps.prototype.just = just 20 | 21 | StreamOps.prototype.scan = function(f, base, fa) { 22 | return fa.scan(f, base) 23 | } 24 | StreamOps.prototype.merge = function(a, b) { 25 | return a.merge(b) 26 | } 27 | 28 | StreamOps.prototype.flatMap = function(f, fa) { 29 | return fa.flatMap(f) 30 | } 31 | 32 | StreamOps.prototype.filter = function (f: (a: A) => boolean, fa: MostStream): MostStream { 33 | return fa.filter(f) 34 | } 35 | 36 | StreamOps.prototype.combine = function ( 37 | f: (...a: any[]) => C, 38 | ...v: MostStream[] 39 | ): MostStream { 40 | return combineArray(f, v) 41 | } 42 | 43 | StreamOps.prototype.subject = function () { 44 | return sync() 45 | } 46 | 47 | StreamOps.prototype.subscribe = function (fa: MostStream, next: (v: A) => void, complete: () => void) { 48 | return fa.recoverWith(x => { 49 | console.error(x) 50 | return fa 51 | }).subscribe({ next, error: x => console.error(x), complete }) as Subscription 52 | } 53 | 54 | StreamOps.prototype.fromPromise = fromPromise; 55 | 56 | 57 | (StreamOps.prototype.from) = from 58 | 59 | export const URI = 'Stream' 60 | export type URI = typeof URI 61 | -------------------------------------------------------------------------------- /src/xs/rx.ts: -------------------------------------------------------------------------------- 1 | import { Observable as RxStream } from '@reactivex/rxjs/dist/package/Observable' 2 | import { Subject } from '@reactivex/rxjs/dist/package/Subject' 3 | import { Subscription, StreamOps } from './index' 4 | import '@reactivex/rxjs/dist/package/add/operator/map' 5 | import '@reactivex/rxjs/dist/package/add/operator/merge' 6 | import '@reactivex/rxjs/dist/package/add/operator/mergeMap' 7 | import '@reactivex/rxjs/dist/package/add/operator/scan' 8 | import '@reactivex/rxjs/dist/package/add/operator/catch' 9 | import '@reactivex/rxjs/dist/package/add/operator/filter' 10 | import '@reactivex/rxjs/dist/package/add/observable/empty' 11 | import '@reactivex/rxjs/dist/package/add/observable/from' 12 | import '@reactivex/rxjs/dist/package/add/observable/of' 13 | import '@reactivex/rxjs/dist/package/add/observable/fromPromise' 14 | import '@reactivex/rxjs/dist/package/add/observable/combineLatest' 15 | 16 | declare module '.' { 17 | interface S_ { 18 | 'RxStream': RxStream 19 | } 20 | } 21 | 22 | declare module '../fantasy/typeclasses' { 23 | interface _ { 24 | 'RxStream': RxStream 25 | } 26 | } 27 | 28 | StreamOps.prototype.empty = RxStream.empty 29 | 30 | StreamOps.prototype.just = RxStream.of 31 | 32 | StreamOps.prototype.scan = function(f, base, fa) { 33 | return fa.scan(f, base) 34 | } 35 | StreamOps.prototype.combine = function ( 36 | f: (...a: any[]) => C, 37 | ...v: RxStream[] 38 | ): RxStream { 39 | return RxStream.combineLatest(v, f) 40 | } 41 | StreamOps.prototype.filter = function (f: (a: A) => boolean, fa: RxStream): RxStream { 42 | return fa.filter(f) 43 | } 44 | StreamOps.prototype.map = function (f: (a: A) => B, fa: RxStream): RxStream { 45 | return fa.map(f) 46 | } 47 | StreamOps.prototype.flatMap = function (f: (a: A) => RxStream, fa: RxStream): RxStream { 48 | return fa.mergeMap(f) 49 | } 50 | StreamOps.prototype.subject = function () { 51 | return new Subject() 52 | } 53 | 54 | StreamOps.prototype.subscribe = function (fa: RxStream, next: (v: A) => void, complete?: () => void) { 55 | return fa.catch(x => { 56 | console.error(x) 57 | return fa 58 | }).subscribe(next, x => console.error(x), complete) as Subscription 59 | } 60 | 61 | StreamOps.prototype.merge = function (a: RxStream, b: RxStream): RxStream { 62 | return a.merge(b) 63 | } 64 | 65 | StreamOps.prototype.fromPromise = RxStream.fromPromise; 66 | 67 | (StreamOps.prototype.from) = RxStream.from 68 | -------------------------------------------------------------------------------- /src/xtests/index.ts: -------------------------------------------------------------------------------- 1 | import rx from './rx' 2 | import most from './most' 3 | export { rx, most } 4 | -------------------------------------------------------------------------------- /src/xtests/most.ts: -------------------------------------------------------------------------------- 1 | import { XREACT_ENGINE } from '../interfaces' 2 | import { doSequence, Test } from './util' 3 | export default class extends Test { 4 | collect(component: any) { 5 | let latestState 6 | if (this.plans != 0) { 7 | latestState = component.context[XREACT_ENGINE].history$.take(this.plans).observe(() => { }) 8 | } else { 9 | latestState = component.context[XREACT_ENGINE].history$.observe(() => { }) 10 | } 11 | doSequence(this.things).then(() => { 12 | if (this.plans == 0) 13 | component.machine.actions.terminate() 14 | }) 15 | 16 | return latestState 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/xtests/rx.ts: -------------------------------------------------------------------------------- 1 | import { XREACT_ENGINE } from '../interfaces' 2 | 3 | import { doSequence, Test } from './util' 4 | export default class extends Test { 5 | collect(component: any) { 6 | let latestState 7 | if (this.plans != 0) { 8 | latestState = component.context[XREACT_ENGINE].history$.take(this.plans).toPromise() 9 | } else { 10 | latestState = component.context[XREACT_ENGINE].history$.toPromise() 11 | } 12 | doSequence(this.things).then(() => { 13 | if (this.plans == 0) 14 | component.machine.actions.terminate() 15 | }) 16 | return latestState 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/xtests/util.ts: -------------------------------------------------------------------------------- 1 | export class Test { 2 | plans: number 3 | things: any[] 4 | constructor() { 5 | this.plans = 0 6 | this.things = [] 7 | } 8 | plan(n: number) { 9 | this.plans = n 10 | return this 11 | } 12 | 13 | do(things: any[]) { 14 | this.things = things 15 | return this 16 | } 17 | } 18 | export function doSequence(tasks: any[]): Promise { 19 | if (tasks.length == 1) 20 | return Promise.resolve(tasks[0]()) 21 | else { 22 | let [head, ...tail] = tasks 23 | return Promise.resolve(head).then(f => { 24 | f(); 25 | return doSequence(tail) 26 | }) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const Nightmare = require('nightmare') 2 | const {expect} = require('chai') 3 | 4 | const nightmare = Nightmare({ show: false }); 5 | 6 | describe('Load Example Page', function() { 7 | this.timeout('30s') 8 | let page, test 9 | before(()=>{ 10 | page = nightmare.goto(`file:///${__dirname}/../docs/src/main/tut/examples/index.html`) 11 | }) 12 | after(()=>{ 13 | return page.end() 14 | }) 15 | 16 | describe('#Example 1', () => { 17 | it('display 30', () => { 18 | return page 19 | .wait('#eg1 .result') 20 | .evaluate(() => document.querySelector('#eg1 .result').textContent) 21 | .then(x=>expect(x).to.equal('30')) 22 | }) 23 | }) 24 | 25 | describe('#Example 2', () => { 26 | it('default 30', () => { 27 | return page 28 | .wait('#eg2 .result') 29 | .evaluate(() => document.querySelector('#eg2 .result').textContent) 30 | .then(x=>expect(x).to.equal('30')) 31 | }) 32 | it('Two number multiply', () => { 33 | return page 34 | .insert('input[name="n1"]', '8') 35 | .insert('input[name="n2"]', '9') 36 | .wait('#eg2 .result') 37 | .evaluate(() => document.querySelector('#eg2 .result').textContent) 38 | .then(x=>expect(x).to.equal('4002')) 39 | }) 40 | }) 41 | 42 | describe('#Example 3', () => { 43 | it('default', () => { 44 | return page 45 | .wait('#eg3 .result') 46 | .evaluate(() => document.querySelector('#eg3 .result').textContent) 47 | .then(x=>expect(x).to.equal('Jichao Ouyang')) 48 | }) 49 | it('reactive concatable', () => { 50 | return page 51 | .insert('input[name="firstName"]', 'Hehe') 52 | .insert('input[name="lastName"]', 'Da') 53 | .wait('#eg3 .result') 54 | .evaluate(() => document.querySelector('#eg3 .result').textContent) 55 | .then(x=>expect(x).to.equal('JichaoHehe OuyangDa')) 56 | }) 57 | }) 58 | 59 | 60 | describe('#Example 4', () => { 61 | it('default', () => { 62 | return page 63 | .wait('#eg4 .result') 64 | .evaluate(() => document.querySelector('#eg4 .result').textContent) 65 | .then(x=>expect(x).to.equal('28')) 66 | }) 67 | it('Traverse', () => { 68 | return page 69 | .insert('input[name="traverse3"]', '1') 70 | .insert('input[name="traverse5"]', '2') 71 | .wait('#eg4 .result') 72 | .evaluate(() => document.querySelector('#eg4 .result').textContent) 73 | .then(x=>expect(x).to.equal('121')) 74 | }) 75 | }) 76 | 77 | describe('#Example 5', () => { 78 | it('default', () => { 79 | return page 80 | .wait(() => document.querySelector('#eg5 .result').textContent == '22.86') 81 | .evaluate(() => document.querySelector('#eg5 .result').textContent) 82 | .then(x=>expect(x).to.equal('22.86')) 83 | }) 84 | }) 85 | 86 | describe('#Example 6', () => { 87 | it('increase 3 by click 3 times', () => { 88 | return page 89 | .click('#eg6 input[name="increment"]') 90 | .click('#eg6 input[name="increment"]') 91 | .click('#eg6 input[name="increment"]') 92 | .evaluate(() => document.querySelector('#eg6 .result').textContent) 93 | .then(x=>expect(x).to.equal('3')) 94 | }) 95 | }) 96 | 97 | describe('#Example 7', () => { 98 | it('increase 3 and decrease 4', () => { 99 | return page 100 | .click('#eg7 input[name="increment"]') 101 | .click('#eg7 input[name="increment"]') 102 | .click('#eg7 input[name="increment"]') 103 | .click('#eg7 input[name="decrement"]') 104 | .click('#eg7 input[name="decrement"]') 105 | .click('#eg7 input[name="decrement"]') 106 | .click('#eg7 input[name="decrement"]') 107 | .evaluate(() => document.querySelector('#eg7 .result').textContent) 108 | .then(x=>expect(x).to.equal('-1')) 109 | }) 110 | }) 111 | 112 | describe('#Example 8', () => { 113 | it('increase 3 and decrease 4', () => { 114 | return page 115 | .click('#eg8 input[name="+1"]') 116 | .click('#eg8 input[name="reset"]') 117 | .click('#eg8 input[name="+1"]') 118 | .click('#eg8 input[name="+1"]') 119 | .click('#eg8 input[name="+1"]') 120 | .click('#eg8 input[name="-1"]') 121 | .click('#eg8 input[name="-1"]') 122 | .click('#eg8 input[name="-1"]') 123 | .click('#eg8 input[name="-1"]') 124 | .evaluate(() => document.querySelector('#eg8 .result').textContent) 125 | .then(x=>expect(x).to.equal('-1')) 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /tsconfig.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "pretty": true, 4 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ 5 | "lib": ["dom", "es2015"], /* Specify library files to be included in the compilation: */ 6 | // "allowJs": true, /* Allow javascript files to be compiled. */ 7 | // "checkJs": true, /* Report errors in .js files. */ 8 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 9 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 10 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 11 | // "outFile": "./", /* Concatenate and emit output to single file. */ 12 | // "outDir": "./", /* Redirect output structure to the directory. */ 13 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 14 | // "removeComments": true, /* Do not emit comments to output. */ 15 | // "noEmit": true, /* Do not emit outputs. */ 16 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 17 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 18 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 19 | 20 | /* Strict Type-Checking Options */ 21 | "strictPropertyInitialization": false, 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 27 | 28 | /* Additional Checks */ 29 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 33 | 34 | /* Module Resolution Options */ 35 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 36 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 39 | // "typeRoots": ["./node_modules/@types"], /* List of folders to include type definitions from. */ 40 | "types": ["node"], /* Type declaration files to be included in compilation. */ 41 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 42 | 43 | /* Source Map Options */ 44 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 45 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 46 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 47 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 48 | 49 | /* Experimental Options */ 50 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 51 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 52 | "target": "es5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.common.json", 3 | "include": [ 4 | "docs/src/main/tut/examples/*.tsx" 5 | ], 6 | "compilerOptions": { 7 | "strict": false, 8 | "noImplicitAny": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.common.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/**/*.tsx" 6 | ], 7 | "exclude": [ 8 | "src/__tests__/**/*" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "lib", 12 | "declarationDir": "lib" 13 | } 14 | } 15 | --------------------------------------------------------------------------------