├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── Readme.md ├── examples ├── counter-list │ ├── .flowconfig │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── counter-list.js │ │ ├── counter.js │ │ └── index.js │ └── type │ │ ├── counter-list.js │ │ └── counter.js ├── counter-pair │ ├── .flowconfig │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── counter-pair.js │ │ ├── counter.js │ │ └── index.js │ └── type │ │ ├── counter-pair.js │ │ └── counter.js ├── counter-set │ ├── .flowconfig │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── counter-list.js │ │ ├── counter-set.js │ │ ├── counter.js │ │ └── index.js │ └── type │ │ ├── counter-list.js │ │ ├── counter-set.js │ │ └── counter.js ├── counter │ ├── .flowconfig │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── counter.js │ │ └── index.js │ └── type │ │ └── counter.js ├── random-gif-list │ ├── .flowconfig │ ├── assets │ │ └── waiting.gif │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── array-find.js │ │ ├── fetch.js │ │ ├── index.js │ │ ├── random-gif-list.js │ │ └── random-gif.js │ └── type │ │ ├── array-find.js │ │ ├── fetch.js │ │ ├── random-gif-list.js │ │ └── random-gif.js ├── random-gif-pair │ ├── .flowconfig │ ├── assets │ │ └── waiting.gif │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── fetch.js │ │ ├── index.js │ │ ├── random-gif-pair.js │ │ └── random-gif.js │ └── type │ │ ├── fetch.js │ │ ├── random-gif-pair.js │ │ └── random-gif.js ├── random-gif │ ├── .flowconfig │ ├── assets │ │ └── waiting.gif │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── fetch.js │ │ ├── index.js │ │ └── random-gif.js │ └── type │ │ ├── fetch.js │ │ └── random-gif.js └── spin-squares │ ├── .flowconfig │ ├── assets │ └── waiting.gif │ ├── gulpfile.babel.js │ ├── index.html │ ├── package.json │ ├── src │ ├── index.js │ ├── spin-square-pair.js │ └── spin-square.js │ └── type │ ├── spin-square-pair.js │ └── spin-square.js ├── interfaces └── dom.js ├── package.json ├── src ├── core.js ├── index.js ├── node.js └── thunk.js └── type └── index.js /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/examples/.* 3 | .*/src/test/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex/lib/.* 6 | .*/node_modules/reflex/dist/.* 7 | .*/reflex-react-driver/lib/.* 8 | .*/reflex-react-driver/dist/.* 9 | 10 | [libs] 11 | ./node_modules/reflex/interfaces/ 12 | 13 | [include] 14 | 15 | [options] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *~ 2 | ~* 3 | !dist 4 | !lib 5 | examples 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - stable 5 | deploy: 6 | provider: npm 7 | email: rfobic@gmail.com 8 | on: 9 | tags: true 10 | api_key: 11 | secure: 0SAU8CJgSNo9jMZub3DRPPpqNnYPoNPUY6lnEunPHDLxwCLOwv7s6fBrSsCOvGifp7OgqMu+gBcZQcmNf5jsLqFnm3y0xEqxkabZs2x6M7FPaY3fEr7G5jDcfgO+cqJtBFsEFgRKGoZt6yhXB8Cy8/Tr/uroyAzHLifWVQURoumfy5GelosB9Tjy1pCThGwoz+20zHUk5amqdgcJiauwsSeDRS3fpOFUn7rLlpcfP44+IzF8szOdNY5wkaL2LDgCOOcK+J5k7X0iHD7E/T7mCzekbg7HCU9Cj+3nou7QsWn/NLxhfVPwa+OCHFdPL88V+kG5hzHj92NMloezafb/jBTG2UaL9QEBoqwA/mRqjcwywT6ekFlt/E2WqaYrQcKBcDRgrp5L7L99E+OczTao0PWKPnBz9xL0/q6mbf560voLArCQznduzmt0ELV4d5rYrtUGxH/Rkt9sO+Oj62mII9ODOcqcEuxH0/8MU3NWh84aajBzBDVjOobNPPv2KmUOfe1BxPUDPw6AR+aphDfEqItHQ3zVsW3Gsx+oetdfDEYIwQhCEPWDZ/hHkRps63UxqoA7r2iRqyUaPT1yvM8KVigvRq5dph8a4w47m8Fz9ckZp/j1l9e53QXAB/EYw/ziqxc9ihc1TJCBKGY61rMWeRzoKehFd4pgjsTJIQIIeU4= 12 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # reflex-react-driver [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] 2 | 3 | This is a [reflex][] application view driver that uses [react][] for rendering into a DOM. 4 | 5 | ## Usage 6 | 7 | ```js 8 | import * as App from "./app" 9 | import {start} from "reflex" 10 | import {Renderer} from "reflex-react-driver" 11 | 12 | const app = start({ 13 | initial: App.initialize(), 14 | update: App.update, 15 | view: App.view 16 | }) 17 | 18 | app.view.subscribe(new Renderer({target: document.body})) 19 | ``` 20 | 21 | [reflex]:https://github.com/Gozala/reflex 22 | [react]:http://facebook.github.io/react/ 23 | 24 | [npm-url]: https://npmjs.org/package/reflex-react-driver 25 | [npm-image]: https://img.shields.io/npm/v/reflex-react-driver.svg?style=flat 26 | 27 | [travis-url]: https://travis-ci.org/Gozala/reflex-react-driver 28 | [travis-image]: https://img.shields.io/travis/Gozala/reflex-react-driver.svg?style=flat 29 | -------------------------------------------------------------------------------- /examples/counter-list/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/counter-list/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/counter-list/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/counter-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-list", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/counter-list/src/counter-list.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as Counter from "./counter"; 4 | import {html, forward, thunk} from "reflex"; 5 | 6 | /*:: 7 | import * as type from "../type/counter-list" 8 | */ 9 | 10 | export const asAdd/*:type.asAdd*/ = () => ({type: "CounterList.Add"}) 11 | export const asRemove/*:type.asRemove*/ = () => ({type: "CounterList.Remove"}) 12 | export const asBy/*:type.asBy*/ = id => act => 13 | ({type: "CounterList.ModifyByID", id, act}) 14 | 15 | 16 | export const create/*:type.create*/ = ({nextID, entries}) => 17 | ({type: "CounterList.Model", nextID, entries}) 18 | 19 | export const add/*:type.add*/ = model => create({ 20 | nextID: model.nextID + 1, 21 | entries: model.entries.concat([{ 22 | type: "CounterList.Entry", 23 | id: model.nextID, 24 | model: Counter.create({value: 0}) 25 | }]) 26 | }) 27 | 28 | export const remove/*:type.remove*/ = model => create({ 29 | nextID: model.nextID, 30 | entries: model.entries.slice(1) 31 | }) 32 | 33 | export const modify/*:type.modify*/ = (model, id, action) => create({ 34 | nextID: model.nextID, 35 | entries: model.entries.map(entry => 36 | entry.id !== id ? 37 | entry : 38 | {type: entry.type, id: id, model: Counter.update(entry.model, action)}) 39 | }) 40 | 41 | export const update/*:type.update*/ = (model, action) => 42 | action.type === "CounterList.Add" ? 43 | add(model, action) : 44 | action.type === "CounterList.Remove" ? 45 | remove(model, action) : 46 | action.type === "CounterList.ModifyByID" ? 47 | modify(model, action.id, action.act) : 48 | model; 49 | 50 | 51 | // View 52 | const viewEntry/*:type.viewEntry*/ = ({id, model}, address) => 53 | html.div({key: id}, [ 54 | Counter.view(model, forward(address, asBy(id))) 55 | ]) 56 | 57 | export const view/*:type.view*/ = (model, address) => 58 | html.div({key: "CounterList"}, [ 59 | html.div({key: "controls"}, [ 60 | html.button({ 61 | key: "remove", 62 | onClick: forward(address, asRemove) 63 | }, ["Remove"]), 64 | html.button({ 65 | key: "add", 66 | onClick: forward(address, asAdd) 67 | }, ["Add"]) 68 | ]), 69 | html.div({ 70 | key: "entries" 71 | }, model.entries.map(entry => thunk(String(entry.id), 72 | viewEntry, 73 | entry, 74 | address))) 75 | ]) 76 | -------------------------------------------------------------------------------- /examples/counter-list/src/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import {html, forward} from "reflex"; 3 | 4 | /*:: 5 | import * as type from "../type/counter" 6 | */ 7 | 8 | export const asIncrement/*:type.asIncrement*/ = () => 9 | ({type: "Counter.Increment"}) 10 | export const asDecrement/*:type.asDecrement*/ = () => 11 | ({type: "Counter.Decrement"}) 12 | 13 | 14 | export const create/*:type.create*/ = ({value}) => 15 | ({type: "Counter.Model", value}) 16 | 17 | export const update/*:type.update*/ = (model, action) => 18 | action.type === "Counter.Increment" ? 19 | {type:model.type, value: model.value + 1} : 20 | action.type === "Counter.Decrement" ? 21 | {type:model.type, value: model.value - 1} : 22 | model 23 | 24 | const counterStyle = { 25 | value: { 26 | fontWeight: "bold" 27 | } 28 | } 29 | 30 | // View 31 | export const view/*:type.view*/ = (model, address) => 32 | html.span({key: "counter"}, [ 33 | html.button({ 34 | key: "decrement", 35 | onClick: forward(address, asDecrement) 36 | }, ["-"]), 37 | html.span({ 38 | key: "value", 39 | style: counterStyle.value, 40 | }, [String(model.value)]), 41 | html.button({ 42 | key: "increment", 43 | onClick: forward(address, asIncrement) 44 | }, ["+"]) 45 | ]) 46 | -------------------------------------------------------------------------------- /examples/counter-list/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as CounterList from "./counter-list" 4 | import {start} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | const app = start({ 8 | initial: CounterList.create(window.app != null ? 9 | window.app.model.value : 10 | {nextID: 0, entries: []}), 11 | update: CounterList.update, 12 | view: CounterList.view 13 | }); 14 | window.app = app 15 | 16 | const renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | -------------------------------------------------------------------------------- /examples/counter-list/type/counter-list.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | import * as Counter from "./counter" 5 | 6 | export type ID = number; 7 | export type Entry = { 8 | type: "CounterList.Entry", 9 | id: ID, 10 | model: Counter.Model 11 | }; 12 | 13 | export type Model = { 14 | type: "CounterList.Model", 15 | nextID: ID, 16 | entries: Array 17 | }; 18 | 19 | 20 | export type Add = {type: "CounterList.Add"} 21 | export type Remove = {type: "CounterList.Remove"} 22 | export type ModifyByID = {type: "CounterList.ModifyByID", 23 | id:ID, 24 | act:Counter.Action} 25 | export type Action = Add|Remove|ModifyByID 26 | 27 | 28 | export type asAdd = () => Add 29 | export type asRemove = () => Remove 30 | export type asBy = (id:ID) => (act:Counter.Action) => ModifyByID 31 | 32 | export type create = (options:{nextID:ID, entries:Array}) => Model 33 | export type add = (model:Model) => Model 34 | export type remove = (model:Model) => Model 35 | export type modify = (model:Model, id:ID, action:Counter.Action) => Model 36 | export type update = (model:Model, action:Action) => Model 37 | 38 | export type viewEntry = (entry:Entry, address:Address) => VirtualNode 39 | export type view = (model:Model, address:Address) => VirtualNode 40 | -------------------------------------------------------------------------------- /examples/counter-list/type/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | 5 | export type Model = {type: "Counter.Model", value:number} 6 | export type Increment = {type: "Counter.Increment"} 7 | export type Decrement = {type: "Counter.Decrement"} 8 | export type Action = Increment|Decrement 9 | 10 | export type asIncrement = () => Increment 11 | export type asDecrement = () => Decrement 12 | 13 | export type create = (options:{value:number}) => Model 14 | export type update = (model:Model, action:Action) => Model 15 | export type view = (model:Model, address:Address) => VirtualNode 16 | -------------------------------------------------------------------------------- /examples/counter-pair/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/counter-pair/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/counter-pair/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/counter-pair/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-pair", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/counter-pair/src/counter-pair.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Counter from "./counter"; 4 | import {html, forward} from "reflex"; 5 | 6 | /*:: 7 | import type {VirtualNode, Address} from "reflex/type" 8 | import * as type from "../type/counter-pair" 9 | */ 10 | 11 | 12 | export const asTop/*:type.asTop*/ = act => 13 | ({type: "CounterPair.Top", act}) 14 | 15 | export const asBottom/*:type.asBottom*/ = act => 16 | ({type: "CounterPair.Bottom", act}) 17 | 18 | export const asReset/*:type.asReset*/ = () => 19 | ({type: "CounterPair.Reset"}) 20 | 21 | 22 | export const create/*:type.create*/ = ({top, bottom}) => ({ 23 | type: "CounterPair.Model", 24 | top: Counter.create(top), 25 | bottom: Counter.create(bottom) 26 | }) 27 | 28 | // Note last two functions are wrapped in too many parenthesis with type 29 | // casting comments at the end due to a bug in type checker: facebook/flow#953 30 | 31 | export const update/*:type.update*/ = (model, action) => 32 | action.type === "CounterPair.Top" ? 33 | create({top: Counter.update(model.top, action.act), 34 | bottom: model.bottom}) : 35 | action.type === "CounterPair.Bottom" ? 36 | create({top: model.top, 37 | bottom: Counter.update(model.bottom, action.act)}) : 38 | action.type === "CounterPair.Reset" ? 39 | create({top: {value: 0}, 40 | bottom: {value: 0}}) : 41 | model 42 | 43 | // View 44 | export const view/*:type.view*/ = (model, address) => 45 | html.div({key: "counter-pair"}, [ 46 | html.div({key: "top"}, [ 47 | Counter.view(model.top, forward(address, asTop)) 48 | ]), 49 | html.div({key: "bottom"}, [ 50 | Counter.view(model.bottom, forward(address, asBottom)), 51 | ]), 52 | html.div({key: "controls"}, [ 53 | html.button({ 54 | key: "reset", 55 | onClick: forward(address, asReset) 56 | }, ["Reset"]) 57 | ]) 58 | ]) 59 | -------------------------------------------------------------------------------- /examples/counter-pair/src/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import {html, forward} from "reflex"; 3 | 4 | /*:: 5 | import * as type from "../type/counter" 6 | */ 7 | 8 | export const asIncrement/*:type.asIncrement*/ = () => 9 | ({type: "Counter.Increment"}) 10 | export const asDecrement/*:type.asDecrement*/ = () => 11 | ({type: "Counter.Decrement"}) 12 | 13 | 14 | export const create/*:type.create*/ = ({value}) => 15 | ({type: "Counter.Model", value}) 16 | 17 | export const update/*:type.update*/ = (model, action) => 18 | action.type === "Counter.Increment" ? 19 | {type:model.type, value: model.value + 1} : 20 | action.type === "Counter.Decrement" ? 21 | {type:model.type, value: model.value - 1} : 22 | model 23 | 24 | const counterStyle = { 25 | value: { 26 | fontWeight: "bold" 27 | } 28 | } 29 | 30 | // View 31 | export const view/*:type.view*/ = (model, address) => 32 | html.span({key: "counter"}, [ 33 | html.button({ 34 | key: "decrement", 35 | onClick: forward(address, asDecrement) 36 | }, ["-"]), 37 | html.span({ 38 | key: "value", 39 | style: counterStyle.value, 40 | }, [String(model.value)]), 41 | html.button({ 42 | key: "increment", 43 | onClick: forward(address, asIncrement) 44 | }, ["+"]) 45 | ]) 46 | -------------------------------------------------------------------------------- /examples/counter-pair/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as CounterPair from "./counter-pair" 4 | import {start} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | var app = start({ 8 | initial: CounterPair.create(window.app != null ? 9 | window.app.model.value : 10 | {top: {value: 0}, 11 | bottom: {value: 0}}), 12 | update: CounterPair.update, 13 | view: CounterPair.view 14 | }); 15 | window.app = app 16 | 17 | var renderer = new Renderer({target: document.body}) 18 | 19 | app.view.subscribe(renderer.address) 20 | -------------------------------------------------------------------------------- /examples/counter-pair/type/counter-pair.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | import * as Counter from "./counter" 5 | 6 | export type Model = { 7 | type: "CounterPair.Model", 8 | top: Counter.Model, 9 | bottom: Counter.Model 10 | } 11 | 12 | export type Top = { 13 | type: "CounterPair.Top", 14 | act: Counter.Action 15 | } 16 | export type Bottom = { 17 | type: "CounterPair.Bottom", 18 | act: any // Workaround for facebook/flow#953 19 | // act: Counter.Action 20 | } 21 | export type Reset = {type: "CounterPair.Reset"} 22 | export type Action 23 | = Top 24 | | Bottom 25 | | Reset 26 | 27 | export type asTop = (action:Counter.Action) => Top 28 | export type asBottom = (action:Counter.Action) => Bottom 29 | export type asReset = () => Reset 30 | 31 | 32 | export type create = (options:{top:{value:number}, bottom:{value:number}}) => 33 | Model 34 | export type update = (model:Model, action:Action) => Model 35 | 36 | 37 | export type view = (model:Model, address:Address) => VirtualNode 38 | -------------------------------------------------------------------------------- /examples/counter-pair/type/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | 5 | export type Model = {type: "Counter.Model", value:number} 6 | export type Increment = {type: "Counter.Increment"} 7 | export type Decrement = {type: "Counter.Decrement"} 8 | export type Action = Increment|Decrement 9 | 10 | export type asIncrement = () => Increment 11 | export type asDecrement = () => Decrement 12 | 13 | export type create = (options:{value:number}) => Model 14 | export type update = (model:Model, action:Action) => Model 15 | export type view = (model:Model, address:Address) => VirtualNode 16 | -------------------------------------------------------------------------------- /examples/counter-set/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/counter-set/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/counter-set/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/counter-set/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-set", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/counter-set/src/counter-list.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as Counter from "./counter"; 4 | import {html, forward, thunk} from "reflex"; 5 | 6 | /*:: 7 | import * as type from "../type/counter-list" 8 | */ 9 | 10 | export const asAdd/*:type.asAdd*/ = () => ({type: "CounterList.Add"}) 11 | export const asRemove/*:type.asRemove*/ = () => ({type: "CounterList.Remove"}) 12 | export const asBy/*:type.asBy*/ = id => act => 13 | ({type: "CounterList.ModifyByID", id, act}) 14 | 15 | 16 | export const create/*:type.create*/ = ({nextID, entries}) => 17 | ({type: "CounterList.Model", nextID, entries}) 18 | 19 | export const add/*:type.add*/ = model => create({ 20 | nextID: model.nextID + 1, 21 | entries: model.entries.concat([{ 22 | type: "CounterList.Entry", 23 | id: model.nextID, 24 | model: Counter.create({value: 0}) 25 | }]) 26 | }) 27 | 28 | export const remove/*:type.remove*/ = model => create({ 29 | nextID: model.nextID, 30 | entries: model.entries.slice(1) 31 | }) 32 | 33 | export const modify/*:type.modify*/ = (model, id, action) => create({ 34 | nextID: model.nextID, 35 | entries: model.entries.map(entry => 36 | entry.id !== id ? 37 | entry : 38 | {type: entry.type, id: id, model: Counter.update(entry.model, action)}) 39 | }) 40 | 41 | export const update/*:type.update*/ = (model, action) => 42 | action.type === "CounterList.Add" ? 43 | add(model, action) : 44 | action.type === "CounterList.Remove" ? 45 | remove(model, action) : 46 | action.type === "CounterList.ModifyByID" ? 47 | modify(model, action.id, action.act) : 48 | model; 49 | 50 | 51 | // View 52 | const viewEntry/*:type.viewEntry*/ = ({id, model}, address) => 53 | html.div({key: id}, [ 54 | Counter.view(model, forward(address, asBy(id))) 55 | ]) 56 | 57 | export const view/*:type.view*/ = (model, address) => 58 | html.div({key: "CounterList"}, [ 59 | html.div({key: "controls"}, [ 60 | html.button({ 61 | key: "remove", 62 | onClick: forward(address, asRemove) 63 | }, ["Remove"]), 64 | html.button({ 65 | key: "add", 66 | onClick: forward(address, asAdd) 67 | }, ["Add"]) 68 | ]), 69 | html.div({ 70 | key: "entries" 71 | }, model.entries.map(entry => thunk(String(entry.id), 72 | viewEntry, 73 | entry, 74 | address))) 75 | ]) 76 | -------------------------------------------------------------------------------- /examples/counter-set/src/counter-set.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as CounterList from "./counter-list"; 4 | import * as Counter from "./counter"; 5 | import {html, forward, thunk} from "reflex"; 6 | 7 | /*:: import * as type from "../type/counter-set" */ 8 | 9 | export const create = CounterList.create 10 | 11 | export const asRemoveBy/*:type.asRemoveBy*/ = id => () => 12 | ({type: "CounterSet.RemoveByID", id}) 13 | 14 | export const removeByID/*:type.removeByID*/ = (model, id) => create({ 15 | nextID: model.nextID, 16 | entries: model.entries.filter(entry => entry.id != id) 17 | }) 18 | 19 | export const update/*:type.update*/ = (model, action) => 20 | action.type === "CounterSet.RemoveByID" ? 21 | removeByID(model, action.id) : 22 | CounterList.update(model, action) 23 | 24 | 25 | const viewEntry/*:type.viewEntry*/ = ({id, model}, address) => 26 | html.div({key: id}, [ 27 | Counter.view(model, forward(address, CounterList.asBy(id))), 28 | html.button({ 29 | key: "remove", 30 | onClick: forward(address, asRemoveBy(id)) 31 | }, ["x"]) 32 | ]) 33 | 34 | export const view/*:type.view*/ = (model, address) => 35 | html.div({key: "CounterList"}, [ 36 | html.div({key: "controls"}, [ 37 | html.button({ 38 | key: "add", 39 | onClick: forward(address, CounterList.asAdd) 40 | }, ["Add"]) 41 | ]), 42 | html.div({ 43 | key: "entries" 44 | }, model.entries.map(entry => thunk(String(entry.id), 45 | viewEntry, 46 | entry, 47 | address))) 48 | ]) 49 | -------------------------------------------------------------------------------- /examples/counter-set/src/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import {html, forward} from "reflex"; 3 | 4 | /*:: 5 | import * as type from "../type/counter" 6 | */ 7 | 8 | export const asIncrement/*:type.asIncrement*/ = () => 9 | ({type: "Counter.Increment"}) 10 | export const asDecrement/*:type.asDecrement*/ = () => 11 | ({type: "Counter.Decrement"}) 12 | 13 | 14 | export const create/*:type.create*/ = ({value}) => 15 | ({type: "Counter.Model", value}) 16 | 17 | export const update/*:type.update*/ = (model, action) => 18 | action.type === "Counter.Increment" ? 19 | {type:model.type, value: model.value + 1} : 20 | action.type === "Counter.Decrement" ? 21 | {type:model.type, value: model.value - 1} : 22 | model 23 | 24 | const counterStyle = { 25 | value: { 26 | fontWeight: "bold" 27 | } 28 | } 29 | 30 | // View 31 | export const view/*:type.view*/ = (model, address) => 32 | html.span({key: "counter"}, [ 33 | html.button({ 34 | key: "decrement", 35 | onClick: forward(address, asDecrement) 36 | }, ["-"]), 37 | html.span({ 38 | key: "value", 39 | style: counterStyle.value, 40 | }, [String(model.value)]), 41 | html.button({ 42 | key: "increment", 43 | onClick: forward(address, asIncrement) 44 | }, ["+"]) 45 | ]) 46 | -------------------------------------------------------------------------------- /examples/counter-set/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as CounterSet from "./counter-set" 4 | import {start} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | var app = start({ 8 | initial: CounterSet.create(window.app != null ? 9 | window.app.model.value : 10 | {nextID: 0, entries: []}), 11 | update: CounterSet.update, 12 | view: CounterSet.view 13 | }); 14 | window.app = app 15 | 16 | var renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | -------------------------------------------------------------------------------- /examples/counter-set/type/counter-list.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | import * as Counter from "./counter" 5 | 6 | export type ID = number; 7 | export type Entry = { 8 | type: "CounterList.Entry", 9 | id: ID, 10 | model: Counter.Model 11 | }; 12 | 13 | export type Model = { 14 | type: "CounterList.Model", 15 | nextID: ID, 16 | entries: Array 17 | }; 18 | 19 | 20 | export type Add = {type: "CounterList.Add"} 21 | export type Remove = {type: "CounterList.Remove"} 22 | export type ModifyByID = {type: "CounterList.ModifyByID", 23 | id:ID, 24 | act:Counter.Action} 25 | export type Action = Add|Remove|ModifyByID 26 | 27 | 28 | export type asAdd = () => Add 29 | export type asRemove = () => Remove 30 | export type asBy = (id:ID) => (act:Counter.Action) => ModifyByID 31 | 32 | export type create = (options:{nextID:ID, entries:Array}) => Model 33 | export type add = (model:Model) => Model 34 | export type remove = (model:Model) => Model 35 | export type modify = (model:Model, id:ID, action:Counter.Action) => Model 36 | export type update = (model:Model, action:Action) => Model 37 | 38 | export type viewEntry = (entry:Entry, address:Address) => VirtualNode 39 | export type view = (model:Model, address:Address) => VirtualNode 40 | -------------------------------------------------------------------------------- /examples/counter-set/type/counter-set.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as Counter from "./counter" 4 | import * as CounterList from "./counter-list" 5 | import type {Address, VirtualNode} from "reflex/type"; 6 | 7 | export type ID = CounterList.ID 8 | export type Entry = CounterList.Entry 9 | export type Model = CounterList.Model 10 | 11 | export type RemoveByID = {type: "CounterSet.RemoveByID", id:ID} 12 | export type Action 13 | = RemoveByID 14 | | CounterList.Action 15 | 16 | export type asRemoveBy = (id:ID) => () => RemoveByID 17 | 18 | 19 | export type update = (model:Model, action:Action) => Model 20 | export type removeByID = (model:Model, id:ID) => Model 21 | 22 | export type viewEntry = (entry:Entry, address:Address) => VirtualNode 23 | export type view = (entry:Model, address:Address) => VirtualNode 24 | -------------------------------------------------------------------------------- /examples/counter-set/type/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | 5 | export type Model = {type: "Counter.Model", value:number} 6 | export type Increment = {type: "Counter.Increment"} 7 | export type Decrement = {type: "Counter.Decrement"} 8 | export type Action = Increment|Decrement 9 | 10 | export type asIncrement = () => Increment 11 | export type asDecrement = () => Decrement 12 | 13 | export type create = (options:{value:number}) => Model 14 | export type update = (model:Model, action:Action) => Model 15 | export type view = (model:Model, address:Address) => VirtualNode 16 | -------------------------------------------------------------------------------- /examples/counter/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/counter/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/counter/src/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import {html, forward} from "reflex"; 3 | 4 | /*:: 5 | import * as type from "../type/counter" 6 | */ 7 | 8 | export const asIncrement/*:type.asIncrement*/ = () => 9 | ({type: "Counter.Increment"}) 10 | export const asDecrement/*:type.asDecrement*/ = () => 11 | ({type: "Counter.Decrement"}) 12 | 13 | 14 | export const create/*:type.create*/ = ({value}) => 15 | ({type: "Counter.Model", value}) 16 | 17 | export const update/*:type.update*/ = (model, action) => 18 | action.type === "Counter.Increment" ? 19 | {type:model.type, value: model.value + 1} : 20 | action.type === "Counter.Decrement" ? 21 | {type:model.type, value: model.value - 1} : 22 | model 23 | 24 | const counterStyle = { 25 | value: { 26 | fontWeight: "bold" 27 | } 28 | } 29 | 30 | // View 31 | export const view/*:type.view*/ = (model, address) => 32 | html.span({key: "counter"}, [ 33 | html.button({ 34 | key: "decrement", 35 | onClick: forward(address, asDecrement) 36 | }, ["-"]), 37 | html.span({ 38 | key: "value", 39 | style: counterStyle.value, 40 | }, [String(model.value)]), 41 | html.button({ 42 | key: "increment", 43 | onClick: forward(address, asIncrement) 44 | }, ["+"]) 45 | ]) 46 | -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as Counter from "./counter" 4 | import {start} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | const app = start({ 8 | initial: Counter.create(window.app != null ? 9 | window.app.model.value : 10 | {value: 0}), 11 | update: Counter.update, 12 | view: Counter.view 13 | }); 14 | window.app = app 15 | 16 | const renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | -------------------------------------------------------------------------------- /examples/counter/type/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Address, VirtualNode} from "reflex/type" 4 | 5 | export type Model = {type: "Counter.Model", value:number} 6 | export type Increment = {type: "Counter.Increment"} 7 | export type Decrement = {type: "Counter.Decrement"} 8 | export type Action = Increment|Decrement 9 | 10 | export type asIncrement = () => Increment 11 | export type asDecrement = () => Decrement 12 | 13 | export type create = (options:{value:number}) => Model 14 | export type update = (model:Model, action:Action) => Model 15 | export type view = (model:Model, address:Address) => VirtualNode 16 | -------------------------------------------------------------------------------- /examples/random-gif-list/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/random-gif-list/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/reflex-react-driver/f507f218f0936ad3a2937c578ad95d6aa927afae/examples/random-gif-list/assets/waiting.gif -------------------------------------------------------------------------------- /examples/random-gif-list/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/random-gif-list/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/random-gif-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-viewer-list", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/random-gif-list/src/array-find.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /*:: import * as type from "../type/array-find" */ 4 | 5 | export const find/*:type.find*/ = Array.prototype.find != null ? 6 | (array, p) => array.find(p) : 7 | (array, p) => { 8 | let index = 0 9 | while (index < array.length) { 10 | if (p(array[index])) { 11 | return array[index] 12 | } 13 | index = index + 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/random-gif-list/src/fetch.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /*:: import * as type from "../type/fetch" */ 4 | 5 | export const fetch/*:type.fetch*/ = global.fetch != null ? 6 | global.fetch : 7 | uri => new Promise((resolve, reject) => { 8 | const request = new XMLHttpRequest() 9 | request.open("GET", uri, true) 10 | request.onload = () => { 11 | const status = request.status === 1223 ? 204 : request.status 12 | if (status < 100 || status > 599) { 13 | reject(Error("Network request failed")) 14 | } else { 15 | resolve({ 16 | status, 17 | statusText: request.statusText, 18 | json() { 19 | return new Promise(resolve => { 20 | resolve(JSON.parse(request.responseText)) 21 | }) 22 | } 23 | }) 24 | } 25 | } 26 | request.onerror = () => { 27 | reject(Error("Network request failed")) 28 | } 29 | request.send() 30 | }) 31 | -------------------------------------------------------------------------------- /examples/random-gif-list/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as RandemGifList from "./random-gif-list" 4 | import {start, Effects} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | var app = start({ 8 | initial: window.app != null ? 9 | [RandemGifList.create(window.app.model.value)] : 10 | RandemGifList.initialize(), 11 | step: RandemGifList.step, 12 | view: RandemGifList.view 13 | }); 14 | window.app = app 15 | 16 | var renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | app.task.subscribe(Effects.service(app.address)) 20 | -------------------------------------------------------------------------------- /examples/random-gif-list/src/random-gif-list.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as RandomGif from "./random-gif" 3 | import {find} from "./array-find" 4 | import {html, forward, thunk, Effects} from "reflex" 5 | 6 | 7 | /*:: import * as type from "../type/random-gif-list" */ 8 | 9 | export const create/*:type.create*/ = ({topic, entries, nextID}) => 10 | ({type: "RandomGifList.Model", topic, entries, nextID}) 11 | 12 | export const initialize/*:type.initialize*/ = () => 13 | [create({topic: "", entries: [], nextID: 0}, Effects.none)] 14 | 15 | export const asTopic/*:type.asTopic*/ = topic => 16 | ({type: "RandomGifList.Topic", topic}) 17 | 18 | export const asCreate/*:type.asCreate*/ = () => 19 | ({type: "RandomGifList.Create"}) 20 | 21 | export const asByID/*:type.asByID*/ = id => act => 22 | ({type: "RandomGifList.UpdateByID", id, act}) 23 | 24 | export const step/*:type.step*/ = (model, action) => { 25 | if (action.type === "RandomGifList.Topic") { 26 | return [ 27 | create({ 28 | topic: action.topic, 29 | nextID: model.nextID, 30 | entries: model.entries 31 | }), 32 | Effects.none 33 | ] 34 | } 35 | 36 | if (action.type === "RandomGifList.Create") { 37 | const [gif, fx] = RandomGif.initialize(model.topic) 38 | return [ 39 | create({ 40 | topic: "", 41 | nextID: model.nextID + 1, 42 | entries: model.entries.concat([{ 43 | type: "RandomGifList.Entry", 44 | id: model.nextID, 45 | model: gif 46 | }]) 47 | }), 48 | fx.map(asByID(model.nextID)) 49 | ] 50 | } 51 | 52 | if (action.type === "RandomGifList.UpdateByID") { 53 | const {id} = action 54 | const {entries, topic, nextID} = model 55 | const entry = find(entries, entry => entry.id === id) 56 | const index = entry != null ? entries.indexOf(entry) : -1 57 | if (index >= 0 && entry != null && entry.model != null && entry.id != null){ 58 | const [gif, fx] = RandomGif.step(entry.model, action.act) 59 | const entries = model.entries.slice(0) 60 | entries[index] = { 61 | type: "RandomGifList.Entry", 62 | id, 63 | model: gif 64 | } 65 | 66 | return [ 67 | create({topic, nextID, entries}), 68 | fx.map(asByID(id)) 69 | ] 70 | } 71 | } 72 | 73 | return [model, Effects.none] 74 | } 75 | 76 | const style = { 77 | input: { 78 | width: "100%", 79 | height: "40px", 80 | padding: "10px 0", 81 | fontSize: "2em", 82 | textAlign: "center" 83 | }, 84 | container: { 85 | display: "flex", 86 | flexWrap: "wrap" 87 | } 88 | } 89 | 90 | export const viewEntry/*:type.viewEntry*/ = ({id, model}, address) => 91 | RandomGif.view(model, forward(address, asByID(id))) 92 | 93 | export const view/*:type.view*/ = (model, address) => 94 | html.div({key: "random-gif-list"}, [ 95 | html.input({ 96 | style: style.input, 97 | placeholder: "What kind of gifs do you want?", 98 | value: model.topic, 99 | onChange: forward(address, event => asTopic(event.target.value)), 100 | onKeyUp: event => { 101 | if (event.keyCode === 13) { 102 | address(asCreate()) 103 | } 104 | } 105 | }), 106 | html.div({key: "random-gifs-list-box", style: style.container}, 107 | model.entries.map(entry => thunk(String(entry.id), 108 | viewEntry, 109 | entry, 110 | address))) 111 | ]) 112 | -------------------------------------------------------------------------------- /examples/random-gif-list/src/random-gif.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {html, forward, Effects, Task} from "reflex" 4 | import {fetch} from "./fetch" 5 | 6 | /*:: import * as type from "../type/random-gif" */ 7 | 8 | export const create/*:type.create*/ = ({topic, uri}) => 9 | ({type: "RandomGif.Model", topic, uri}) 10 | 11 | export const initialize/*:type.initialize*/ = topic => 12 | [create({topic, uri: "assets/waiting.gif"}), getRandomGif(topic)] 13 | 14 | 15 | export const asRequestMore/*:type.asRequestMore*/ = () => 16 | ({type: "RandomGif.RequestMore"}) 17 | 18 | export const asReceiveNewGif/*:type.asReceiveNewGif*/ = uri => 19 | ({type: "RandomGif.ReceiveNewGif", uri}) 20 | 21 | 22 | export const step/*:type.step*/ = (model, action) => 23 | action.type === "RandomGif.RequestMore" ? 24 | [model, getRandomGif(model.topic)] : 25 | action.type === "RandomGif.ReceiveNewGif" ? 26 | [ 27 | create({ 28 | topic: model.topic, 29 | uri: action.uri != null ? action.uri : model.uri 30 | }), 31 | Effects.none 32 | ] : 33 | [model, Effects.none] 34 | 35 | const makeRandomURI = topic => 36 | `http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}` 37 | 38 | const decodeResponseBody = body => 39 | (body != null && body.data != null && body.data.image_url != null) ? 40 | String(body.data.image_url) : 41 | null 42 | 43 | const readResponseAsJSON = response => response.json() 44 | 45 | export const getRandomGif/*:type.getRandomGif*/ = topic => 46 | Effects.task(Task.future(() => fetch(makeRandomURI(topic)) 47 | .then(readResponseAsJSON) 48 | .then(decodeResponseBody) 49 | .then(asReceiveNewGif))) 50 | 51 | const style = { 52 | viewer: { 53 | width: "200px" 54 | }, 55 | header: { 56 | width: "200px", 57 | textAlign: "center" 58 | }, 59 | image(uri) { 60 | return { 61 | display: "inline-block", 62 | width: "200px", 63 | height: "200px", 64 | backgroundPosition: "center center", 65 | backgroundSize: "cover", 66 | backgroundImage: `url("${uri}")` 67 | } 68 | } 69 | } 70 | 71 | export const view/*:type.view*/ = (model, address) => 72 | html.div({key: "gif-viewer", style: style.viewer}, [ 73 | html.h2({key: "header", style: style.header}, [model.topic]), 74 | html.div({key: "image", style: style.image(model.uri)}), 75 | html.button({key: "button", onClick: forward(address, asRequestMore)}, [ 76 | "More please!" 77 | ]) 78 | ]) 79 | -------------------------------------------------------------------------------- /examples/random-gif-list/type/array-find.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type find = (array:Array, p:(a:a) => boolean) => ?a 4 | -------------------------------------------------------------------------------- /examples/random-gif-list/type/fetch.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type JSONValue 4 | = string 5 | | number 6 | | void 7 | | {[key:string]: JSONValue} 8 | 9 | 10 | export type Response = { 11 | status: number, 12 | statusText: string, 13 | json: ()=> Promise 14 | } 15 | 16 | export type fetch = (uri:string) => Promise 17 | -------------------------------------------------------------------------------- /examples/random-gif-list/type/random-gif-list.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Effects} from "reflex/type/effects" 4 | import type {Address, VirtualNode} from "reflex/type" 5 | import * as RandomGif from "./random-gif" 6 | 7 | export type ID = number 8 | 9 | export type Entry = { 10 | type: "RandomGifList.Entry", 11 | id: ID, 12 | model: RandomGif.Model 13 | } 14 | 15 | export type State = { 16 | topic: string, 17 | nextID: ID, 18 | entries: Array 19 | } 20 | 21 | export type Model = { 22 | type: "RandomGifList.Model", 23 | topic: string, 24 | nextID: ID, 25 | entries: Array 26 | } 27 | 28 | 29 | export type Topic = {type: "RandomGifList.Topic", topic: string} 30 | export type Create = {type: "RandomGifList.Create"} 31 | export type UpdateByID = { 32 | type: "RandomGifList.UpdateByID", 33 | id: ID, 34 | act: RandomGif.Action 35 | } 36 | 37 | export type Action 38 | = Topic 39 | | Create 40 | | UpdateByID 41 | 42 | 43 | 44 | export type asTopic = (topic:string) => Topic 45 | export type asCreate = () => Create 46 | export type asByID = (id:ID) => (act:RandomGif.Action) => UpdateByID 47 | 48 | export type create = (options:State) => Model 49 | export type initialize = () => [Model, Effects] 50 | 51 | export type step = (model:Model, action:Action) => [Model, Effects] 52 | 53 | export type viewEntry = (entry:Entry, address:Address) => VirtualNode 54 | export type view = (model:Model, address:Address) => VirtualNode 55 | -------------------------------------------------------------------------------- /examples/random-gif-list/type/random-gif.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Effects} from "reflex/type/effects" 4 | import type {Address, VirtualNode} from "reflex/type" 5 | 6 | export type State = { 7 | topic: string, 8 | uri: string 9 | } 10 | 11 | export type Model 12 | = {type: "RandomGif.Model"} 13 | & State 14 | 15 | export type RequestMore = {type: "RandomGif.RequestMore"} 16 | export type ReceiveNewGif = {type: "RandomGif.ReceiveNewGif", uri: ?string} 17 | export type Action 18 | = RequestMore 19 | | ReceiveNewGif 20 | 21 | export type asRequestMore = () => RequestMore 22 | export type asReceiveNewGif = (uri:?string) => ReceiveNewGif 23 | 24 | export type create = (options:State) => Model 25 | 26 | 27 | export type initialize = (topic:string) => [Model, Effects] 28 | export type step = (model:Model, action:Action) => [Model, Effects] 29 | 30 | export type getRandomGif = (topic:string) => Effects 31 | 32 | export type view = (model:Model, address:Address) => VirtualNode 33 | -------------------------------------------------------------------------------- /examples/random-gif-pair/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/random-gif-pair/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/reflex-react-driver/f507f218f0936ad3a2937c578ad95d6aa927afae/examples/random-gif-pair/assets/waiting.gif -------------------------------------------------------------------------------- /examples/random-gif-pair/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/random-gif-pair/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/random-gif-pair/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-viewer-pair", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/random-gif-pair/src/fetch.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /*:: import * as type from "../type/fetch" */ 4 | 5 | export const fetch/*:type.fetch*/ = global.fetch != null ? 6 | global.fetch : 7 | uri => new Promise((resolve, reject) => { 8 | const request = new XMLHttpRequest() 9 | request.open("GET", uri, true) 10 | request.onload = () => { 11 | const status = request.status === 1223 ? 204 : request.status 12 | if (status < 100 || status > 599) { 13 | reject(Error("Network request failed")) 14 | } else { 15 | resolve({ 16 | status, 17 | statusText: request.statusText, 18 | json() { 19 | return new Promise(resolve => { 20 | resolve(JSON.parse(request.responseText)) 21 | }) 22 | } 23 | }) 24 | } 25 | } 26 | request.onerror = () => { 27 | reject(Error("Network request failed")) 28 | } 29 | request.send() 30 | }) 31 | -------------------------------------------------------------------------------- /examples/random-gif-pair/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as RandomGifPair from "./random-gif-pair" 4 | import {start, Effects} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | var app = start({ 8 | initial: window.app != null ? 9 | [RandomGifPair.create(window.app.model.value)] : 10 | RandomGifPair.initialize("funny cats", "funny hamsters"), 11 | step: RandomGifPair.step, 12 | view: RandomGifPair.view 13 | }); 14 | window.app = app 15 | 16 | var renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | app.task.subscribe(Effects.service(app.address)) 20 | -------------------------------------------------------------------------------- /examples/random-gif-pair/src/random-gif-pair.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as RandomGif from "./random-gif" 4 | import {html, forward, Task, Effects} from "reflex" 5 | 6 | /*:: import * as type from "../type/random-gif-pair" */ 7 | 8 | export const create/*:type.create*/ = ({left, right}) => ({ 9 | type: "RandomGifPair.Model", 10 | left: RandomGif.create(left), 11 | right: RandomGif.create(right) 12 | }) 13 | 14 | export const initialize/*:type.initialize*/ = (leftTopic, rightTopic) => { 15 | const [left, leftFx] = RandomGif.initialize(leftTopic) 16 | const [right, rightFx] = RandomGif.initialize(rightTopic) 17 | return [ 18 | create({left, right}), 19 | Effects.batch([ 20 | leftFx.map(asLeft), 21 | rightFx.map(asRight) 22 | ]) 23 | ] 24 | } 25 | 26 | export const asLeft/*:type.asLeft*/ = act => 27 | ({type: "RandomGifPair.Left", act}) 28 | 29 | export const asRight/*:type.asRight*/ = act => 30 | ({type: "RandomGifPair.Right", act}) 31 | 32 | 33 | export const step/*:type.step*/ = (model, action) => { 34 | if (action.type === "RandomGifPair.Left") { 35 | const [left, fx] = RandomGif.step(model.left, action.act) 36 | return [create({left, right: model.right}), fx.map(asLeft)] 37 | } 38 | 39 | if (action.type === "RandomGifPair.Right") { 40 | const [right, fx] = RandomGif.step(model.right, action.act) 41 | return [create({left:model.left, right}), fx.map(asRight)] 42 | } 43 | 44 | return [model, Effects.none] 45 | } 46 | 47 | export const view/*:type.view*/ = (model, address) => { 48 | return html.div({key: "random-gif-pair", 49 | style: {display: "flex"}}, [ 50 | html.div({key: "left"}, [ 51 | RandomGif.view(model.left, forward(address, asLeft)) 52 | ]), 53 | html.div({key: "right"}, [ 54 | RandomGif.view(model.right, forward(address, asRight)) 55 | ]) 56 | ]); 57 | }; 58 | -------------------------------------------------------------------------------- /examples/random-gif-pair/src/random-gif.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {html, forward, Effects, Task} from "reflex" 4 | import {fetch} from "./fetch" 5 | 6 | /*:: import * as type from "../type/random-gif" */ 7 | 8 | export const create/*:type.create*/ = ({topic, uri}) => 9 | ({type: "RandomGif.Model", topic, uri}) 10 | 11 | export const initialize/*:type.initialize*/ = topic => 12 | [create({topic, uri: "assets/waiting.gif"}), getRandomGif(topic)] 13 | 14 | 15 | export const asRequestMore/*:type.asRequestMore*/ = () => 16 | ({type: "RandomGif.RequestMore"}) 17 | 18 | export const asReceiveNewGif/*:type.asReceiveNewGif*/ = uri => 19 | ({type: "RandomGif.ReceiveNewGif", uri}) 20 | 21 | 22 | export const step/*:type.step*/ = (model, action) => 23 | action.type === "RandomGif.RequestMore" ? 24 | [model, getRandomGif(model.topic)] : 25 | action.type === "RandomGif.ReceiveNewGif" ? 26 | [ 27 | create({ 28 | topic: model.topic, 29 | uri: action.uri != null ? action.uri : model.uri 30 | }), 31 | Effects.none 32 | ] : 33 | [model, Effects.none] 34 | 35 | const makeRandomURI = topic => 36 | `http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}` 37 | 38 | const decodeResponseBody = body => 39 | (body != null && body.data != null && body.data.image_url != null) ? 40 | String(body.data.image_url) : 41 | null 42 | 43 | const readResponseAsJSON = response => response.json() 44 | 45 | export const getRandomGif/*:type.getRandomGif*/ = topic => 46 | Effects.task(Task.future(() => fetch(makeRandomURI(topic)) 47 | .then(readResponseAsJSON) 48 | .then(decodeResponseBody) 49 | .then(asReceiveNewGif))) 50 | 51 | const style = { 52 | viewer: { 53 | width: "200px" 54 | }, 55 | header: { 56 | width: "200px", 57 | textAlign: "center" 58 | }, 59 | image(uri) { 60 | return { 61 | display: "inline-block", 62 | width: "200px", 63 | height: "200px", 64 | backgroundPosition: "center center", 65 | backgroundSize: "cover", 66 | backgroundImage: `url("${uri}")` 67 | } 68 | } 69 | } 70 | 71 | export const view/*:type.view*/ = (model, address) => 72 | html.div({key: "gif-viewer", style: style.viewer}, [ 73 | html.h2({key: "header", style: style.header}, [model.topic]), 74 | html.div({key: "image", style: style.image(model.uri)}), 75 | html.button({key: "button", onClick: forward(address, asRequestMore)}, [ 76 | "More please!" 77 | ]) 78 | ]) 79 | -------------------------------------------------------------------------------- /examples/random-gif-pair/type/fetch.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type JSONValue 4 | = string 5 | | number 6 | | void 7 | | {[key:string]: JSONValue} 8 | 9 | 10 | export type Response = { 11 | status: number, 12 | statusText: string, 13 | json: ()=> Promise 14 | } 15 | 16 | export type fetch = (uri:string) => Promise 17 | -------------------------------------------------------------------------------- /examples/random-gif-pair/type/random-gif-pair.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Effects} from "reflex/type/effects" 4 | import type {Address, VirtualNode} from "reflex/type" 5 | import * as RandomGif from "./random-gif" 6 | 7 | export type State = { 8 | left: RandomGif.State, 9 | right: RandomGif.State 10 | } 11 | 12 | export type Model 13 | = {type: "RandomGifPair.Model"} 14 | & {left: RandomGif.Model, right:RandomGif.Model} 15 | 16 | export type Left = { 17 | type: "RandomGifPair.Left", 18 | act: RandomGif.Action 19 | } 20 | export type Right = { 21 | type: "RandomGifPair.Right", 22 | act: any // Workaround for facebook/flow#953 23 | // act: RandomGif.Action 24 | } 25 | export type Action = Left | Right 26 | 27 | export type asLeft = (action:RandomGif.Action) => Left 28 | export type asRight = (action:RandomGif.Action) => Right 29 | 30 | export type create = (options:State) => Model 31 | export type initialize = (leftTopic:string, rightTopic:string) => 32 | [Model, Effects] 33 | 34 | export type step = (model:Model, action:Action) => [Model, Effects] 35 | 36 | export type view = (model:Model, address:Address) => VirtualNode 37 | -------------------------------------------------------------------------------- /examples/random-gif-pair/type/random-gif.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Effects} from "reflex/type/effects" 4 | import type {Address, VirtualNode} from "reflex/type" 5 | 6 | export type State = { 7 | topic: string, 8 | uri: string 9 | } 10 | 11 | export type Model 12 | = {type: "RandomGif.Model"} 13 | & State 14 | 15 | export type RequestMore = {type: "RandomGif.RequestMore"} 16 | export type ReceiveNewGif = {type: "RandomGif.ReceiveNewGif", uri: ?string} 17 | export type Action 18 | = RequestMore 19 | | ReceiveNewGif 20 | 21 | export type asRequestMore = () => RequestMore 22 | export type asReceiveNewGif = (uri:?string) => ReceiveNewGif 23 | 24 | export type create = (options:State) => Model 25 | 26 | 27 | export type initialize = (topic:string) => [Model, Effects] 28 | export type step = (model:Model, action:Action) => [Model, Effects] 29 | 30 | export type getRandomGif = (topic:string) => Effects 31 | 32 | export type view = (model:Model, address:Address) => VirtualNode 33 | -------------------------------------------------------------------------------- /examples/random-gif/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | 8 | [libs] 9 | ./node_modules/reflex/interfaces/ 10 | ./node_modules/reflex-react-driver/interfaces/ 11 | 12 | [include] 13 | 14 | [options] 15 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 16 | module.name_mapper='reflex' -> 'reflex/src/index' 17 | -------------------------------------------------------------------------------- /examples/random-gif/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/reflex-react-driver/f507f218f0936ad3a2937c578ad95d6aa927afae/examples/random-gif/assets/waiting.gif -------------------------------------------------------------------------------- /examples/random-gif/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/random-gif/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/random-gif/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-viewer", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "reflex": "latest", 11 | "reflex-react-driver": "latest" 12 | }, 13 | "devDependencies": { 14 | "browserify": "11.0.1", 15 | "watchify": "3.3.1", 16 | 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | 21 | "babel-core": "5.8.23", 22 | "babel-runtime": "5.8.20", 23 | "ecstatic": "0.8.0", 24 | "flow-bin": "0.17.0", 25 | 26 | "gulp": "3.9.0", 27 | "gulp-sequence": "0.4.1", 28 | "gulp-sourcemaps": "1.5.2", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.6", 31 | "vinyl-buffer": "1.0.0", 32 | "vinyl-source-stream": "1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/random-gif/src/fetch.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /*:: import * as type from "../type/fetch" */ 4 | 5 | export const fetch/*:type.fetch*/ = global.fetch != null ? 6 | global.fetch : 7 | uri => new Promise((resolve, reject) => { 8 | const request = new XMLHttpRequest() 9 | request.open("GET", uri, true) 10 | request.onload = () => { 11 | const status = request.status === 1223 ? 204 : request.status 12 | if (status < 100 || status > 599) { 13 | reject(Error("Network request failed")) 14 | } else { 15 | resolve({ 16 | status, 17 | statusText: request.statusText, 18 | json() { 19 | return new Promise(resolve => { 20 | resolve(JSON.parse(request.responseText)) 21 | }) 22 | } 23 | }) 24 | } 25 | } 26 | request.onerror = () => { 27 | reject(Error("Network request failed")) 28 | } 29 | request.send() 30 | }) 31 | -------------------------------------------------------------------------------- /examples/random-gif/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as RandomGif from "./random-gif" 4 | import {start, Effects} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | var app = start({ 8 | initial: window.app != null ? 9 | [RandomGif.create(window.app.model.value)] : 10 | RandomGif.initialize("funny cats"), 11 | step: RandomGif.step, 12 | view: RandomGif.view 13 | }); 14 | window.app = app 15 | 16 | var renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | app.task.subscribe(Effects.service(app.address)) 20 | -------------------------------------------------------------------------------- /examples/random-gif/src/random-gif.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {html, forward, Effects, Task} from "reflex" 4 | import {fetch} from "./fetch" 5 | 6 | /*:: import * as type from "../type/random-gif" */ 7 | 8 | export const create/*:type.create*/ = ({topic, uri}) => 9 | ({type: "RandomGif.Model", topic, uri}) 10 | 11 | export const initialize/*:type.initialize*/ = topic => 12 | [create({topic, uri: "assets/waiting.gif"}), getRandomGif(topic)] 13 | 14 | 15 | export const asRequestMore/*:type.asRequestMore*/ = () => 16 | ({type: "RandomGif.RequestMore"}) 17 | 18 | export const asReceiveNewGif/*:type.asReceiveNewGif*/ = uri => 19 | ({type: "RandomGif.ReceiveNewGif", uri}) 20 | 21 | 22 | export const step/*:type.step*/ = (model, action) => 23 | action.type === "RandomGif.RequestMore" ? 24 | [model, getRandomGif(model.topic)] : 25 | action.type === "RandomGif.ReceiveNewGif" ? 26 | [ 27 | create({ 28 | topic: model.topic, 29 | uri: action.uri != null ? action.uri : model.uri 30 | }), 31 | Effects.none 32 | ] : 33 | [model, Effects.none] 34 | 35 | const makeRandomURI = topic => 36 | `http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}` 37 | 38 | const decodeResponseBody = body => 39 | (body != null && body.data != null && body.data.image_url != null) ? 40 | String(body.data.image_url) : 41 | null 42 | 43 | const readResponseAsJSON = response => response.json() 44 | 45 | export const getRandomGif/*:type.getRandomGif*/ = topic => 46 | Effects.task(Task.future(() => fetch(makeRandomURI(topic)) 47 | .then(readResponseAsJSON) 48 | .then(decodeResponseBody) 49 | .then(asReceiveNewGif))) 50 | 51 | const style = { 52 | viewer: { 53 | width: "200px" 54 | }, 55 | header: { 56 | width: "200px", 57 | textAlign: "center" 58 | }, 59 | image(uri) { 60 | return { 61 | display: "inline-block", 62 | width: "200px", 63 | height: "200px", 64 | backgroundPosition: "center center", 65 | backgroundSize: "cover", 66 | backgroundImage: `url("${uri}")` 67 | } 68 | } 69 | } 70 | 71 | export const view/*:type.view*/ = (model, address) => 72 | html.div({key: "gif-viewer", style: style.viewer}, [ 73 | html.h2({key: "header", style: style.header}, [model.topic]), 74 | html.div({key: "image", style: style.image(model.uri)}), 75 | html.button({key: "button", onClick: forward(address, asRequestMore)}, [ 76 | "More please!" 77 | ]) 78 | ]) 79 | -------------------------------------------------------------------------------- /examples/random-gif/type/fetch.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type JSONValue 4 | = string 5 | | number 6 | | void 7 | | {[key:string]: JSONValue} 8 | 9 | 10 | export type Response = { 11 | status: number, 12 | statusText: string, 13 | json: ()=> Promise 14 | } 15 | 16 | export type fetch = (uri:string) => Promise 17 | -------------------------------------------------------------------------------- /examples/random-gif/type/random-gif.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Effects} from "reflex/type/effects" 4 | import type {Address, VirtualNode} from "reflex/type" 5 | 6 | export type State = { 7 | topic: string, 8 | uri: string 9 | } 10 | 11 | export type Model 12 | = {type: "RandomGif.Model"} 13 | & State 14 | 15 | export type RequestMore = {type: "RandomGif.RequestMore"} 16 | export type ReceiveNewGif = {type: "RandomGif.ReceiveNewGif", uri: ?string} 17 | export type Action 18 | = RequestMore 19 | | ReceiveNewGif 20 | 21 | export type asRequestMore = () => RequestMore 22 | export type asReceiveNewGif = (uri:?string) => ReceiveNewGif 23 | 24 | export type create = (options:State) => Model 25 | 26 | 27 | export type initialize = (topic:string) => [Model, Effects] 28 | export type step = (model:Model, action:Action) => [Model, Effects] 29 | 30 | export type getRandomGif = (topic:string) => Effects 31 | 32 | export type view = (model:Model, address:Address) => VirtualNode 33 | -------------------------------------------------------------------------------- /examples/spin-squares/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/test/.* 3 | .*/dist/.* 4 | .*/node_modules/reflex/examples/.* 5 | .*/node_modules/reflex-react-driver/lib/.* 6 | .*/node_modules/reflex/lib/.* 7 | .*/node_modules/eased/lib/.* 8 | .*/node_modules/eased/dist/.* 9 | .*/node_modules/eased/.*/lib/.* 10 | .*/node_modules/eased/.*/dist/.* 11 | 12 | [libs] 13 | ./node_modules/reflex/interfaces/ 14 | ./node_modules/reflex-react-driver/interfaces/ 15 | 16 | [include] 17 | 18 | [options] 19 | module.name_mapper='reflex-react-driver' -> 'reflex-react-driver/src/index' 20 | module.name_mapper='reflex' -> 'reflex/src/index' 21 | module.name_mapper='eased' -> 'eased/src/index' 22 | module.name_mapper='color-structure' -> 'color-structure/src/index' 23 | -------------------------------------------------------------------------------- /examples/spin-squares/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/reflex-react-driver/f507f218f0936ad3a2937c578ad95d6aa927afae/examples/spin-squares/assets/waiting.gif -------------------------------------------------------------------------------- /examples/spin-squares/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import browserify from 'browserify'; 4 | import gulp from 'gulp'; 5 | import source from 'vinyl-source-stream'; 6 | import buffer from 'vinyl-buffer'; 7 | import uglify from 'gulp-uglify'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | import gutil from 'gulp-util'; 10 | import watchify from 'watchify'; 11 | import child from 'child_process'; 12 | import http from 'http'; 13 | import path from 'path'; 14 | import babelify from 'babelify'; 15 | import sequencial from 'gulp-sequence'; 16 | import ecstatic from 'ecstatic'; 17 | import hmr from 'browserify-hmr'; 18 | import hotify from 'hotify'; 19 | 20 | var settings = { 21 | port: process.env.DEV_PORT || '6061', 22 | cache: {}, 23 | plugin: [], 24 | transform: [ 25 | babelify.configure({ 26 | "optional": [ 27 | "spec.protoToAssign", 28 | "runtime" 29 | ], 30 | "blacklist": [] 31 | }) 32 | ], 33 | debug: true, 34 | watch: false, 35 | compression: null 36 | }; 37 | 38 | var Bundler = function(entry) { 39 | this.entry = entry 40 | this.compression = settings.compression 41 | this.build = this.build.bind(this); 42 | 43 | this.bundler = browserify({ 44 | entries: ['./src/' + entry], 45 | debug: settings.debug, 46 | cache: {}, 47 | transform: settings.transform, 48 | plugin: settings.plugin 49 | }); 50 | 51 | this.watcher = settings.watch && 52 | watchify(this.bundler) 53 | .on('update', this.build); 54 | } 55 | Bundler.prototype.bundle = function() { 56 | gutil.log(`Begin bundling: '${this.entry}'`); 57 | return this.watcher ? this.watcher.bundle() : this.bundler.bundle(); 58 | } 59 | 60 | Bundler.prototype.build = function() { 61 | var bundle = this 62 | .bundle() 63 | .on('error', (error) => { 64 | gutil.beep(); 65 | console.error(`Failed to browserify: '${this.entry}'`, error.message); 66 | }) 67 | .pipe(source(this.entry + '.js')) 68 | .pipe(buffer()) 69 | .pipe(sourcemaps.init({loadMaps: true})) 70 | .on('error', (error) => { 71 | gutil.beep(); 72 | console.error(`Failed to make source maps for: '${this.entry}'`, 73 | error.message); 74 | }); 75 | 76 | return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle) 77 | .on('error', (error) => { 78 | gutil.beep(); 79 | console.error(`Failed to bundle: '${this.entry}'`, 80 | error.message); 81 | }) 82 | .pipe(sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .on('end', () => { 85 | gutil.log(`Completed bundling: '${this.entry}'`); 86 | }); 87 | } 88 | 89 | var bundler = function(entry) { 90 | return gulp.task(entry, function() { 91 | return new Bundler(entry).build(); 92 | }); 93 | } 94 | 95 | // Starts a static http server that serves browser.html directory. 96 | gulp.task('server', function() { 97 | var server = http.createServer(ecstatic({ 98 | root: path.join(module.filename, '../'), 99 | cache: 0 100 | })); 101 | server.listen(settings.port); 102 | }); 103 | 104 | gulp.task('compressor', function() { 105 | settings.compression = { 106 | mangle: true, 107 | compress: true, 108 | acorn: true 109 | }; 110 | }); 111 | 112 | gulp.task('watcher', function() { 113 | settings.watch = true 114 | }); 115 | 116 | gulp.task('hotreload', function() { 117 | settings.plugin.push(hmr); 118 | settings.transform.push(hotify); 119 | }); 120 | 121 | bundler('index'); 122 | 123 | gulp.task('build', [ 124 | 'compressor', 125 | 'index' 126 | ]); 127 | 128 | gulp.task('watch', [ 129 | 'watcher', 130 | 'index' 131 | ]); 132 | 133 | gulp.task('develop', sequencial('watch', 'server')); 134 | gulp.task('live', ['hotreload', 'develop']); 135 | gulp.task('default', ['live']); 136 | -------------------------------------------------------------------------------- /examples/spin-squares/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample App 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/spin-squares/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spin-squares", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "test": "flow check", 6 | "start": "gulp live", 7 | "build": "NODE_ENV=production gulp build" 8 | }, 9 | "dependencies": { 10 | "eased": "0.0.3", 11 | "reflex": "latest", 12 | "reflex-react-driver": "latest" 13 | }, 14 | "devDependencies": { 15 | "browserify": "11.0.1", 16 | "watchify": "3.3.1", 17 | "babelify": "6.1.3", 18 | "browserify-hmr": "0.3.0", 19 | "hotify": "0.0.1", 20 | "babel-core": "5.8.23", 21 | "babel-runtime": "5.8.20", 22 | "ecstatic": "0.8.0", 23 | "flow-bin": "0.17.0", 24 | "gulp": "3.9.0", 25 | "gulp-sequence": "0.4.1", 26 | "gulp-sourcemaps": "1.5.2", 27 | "gulp-uglify": "^1.2.0", 28 | "gulp-util": "^3.0.6", 29 | "vinyl-buffer": "1.0.0", 30 | "vinyl-source-stream": "1.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/spin-squares/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as SpinSquarePair from "./spin-square-pair" 4 | import {start, Effects} from "reflex" 5 | import {Renderer} from "reflex-react-driver" 6 | 7 | var app = start({ 8 | initial: window.app != null ? 9 | [SpinSquarePair.create(window.app.model.value)] : 10 | SpinSquarePair.initialize(), 11 | step: SpinSquarePair.step, 12 | view: SpinSquarePair.view 13 | }); 14 | window.app = app 15 | 16 | var renderer = new Renderer({target: document.body}) 17 | 18 | app.view.subscribe(renderer.address) 19 | app.task.subscribe(Effects.service(app.address)) 20 | -------------------------------------------------------------------------------- /examples/spin-squares/src/spin-square-pair.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as SpinSquare from "./spin-square" 4 | import {html, forward, thunk, Effects} from "reflex" 5 | 6 | /*:: import * as type from "../type/spin-square-pair" */ 7 | 8 | export const create/*:type.create*/ = ({left, right}) => ({ 9 | type: "SpinSquarePair.Model", 10 | left: SpinSquare.create(left), 11 | right: SpinSquare.create(right) 12 | }) 13 | 14 | export const initialize/*:type.initialize*/ = () => { 15 | const [left, leftFx] = SpinSquare.initialize() 16 | const [right, rightFx] = SpinSquare.initialize() 17 | return [ 18 | create({left, right}), 19 | Effects.batch([ 20 | leftFx.map(asLeft), 21 | rightFx.map(asRight) 22 | ]) 23 | ] 24 | } 25 | 26 | export const asLeft/*:type.asLeft*/ = act => 27 | ({type: "SpinSquarePair.Left", act}) 28 | 29 | export const asRight/*:type.asRight*/ = act => 30 | ({type: "SpinSquarePair.Right", act}) 31 | 32 | 33 | export const step/*:type.step*/ = (model, action) => { 34 | if (action.type === "SpinSquarePair.Left") { 35 | const [left, fx] = SpinSquare.step(model.left, action.act) 36 | return [create({left, right: model.right}), fx.map(asLeft)] 37 | } 38 | if (action.type === "SpinSquarePair.Right") { 39 | const [right, fx] = SpinSquare.step(model.right, action.act) 40 | return [create({left:model.left, right}), fx.map(asRight)] 41 | } 42 | 43 | return [model, Effects.none] 44 | } 45 | 46 | 47 | export var view/*:type.view*/ = (model, address) => 48 | html.div({key: "spin-square-pair", 49 | style: {display: "flex"}}, [ 50 | thunk("left", SpinSquare.view, model.left, forward(address, asLeft)), 51 | thunk("right", SpinSquare.view, model.right, forward(address, asRight)) 52 | ]) 53 | -------------------------------------------------------------------------------- /examples/spin-squares/src/spin-square.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import {Record, Union} from "typed-immutable" 3 | import {html, forward, Effects, Task} from "reflex" 4 | import {ease, easeOutBounce, float} from "eased" 5 | 6 | /*:: import * as type from "../type/spin-square" */ 7 | 8 | export const create/*:type.create*/ = ({angle, animationState}) => 9 | ({type: "SpinSquare.Model", angle, animationState}) 10 | 11 | export const initialize/*:type.initialize*/ = () => [ 12 | create({angle: 0, animationState:null}), 13 | Effects.none 14 | ] 15 | 16 | const rotateStep = 90 17 | const ms = 1 18 | const second = 1000 * ms 19 | const duration = second 20 | 21 | export const asSpin/*:type.asSpin*/ = () => ({type: "SpinSquare.Spin"}) 22 | export const asTick/*:type.asTick*/ = time => ({type: "SpinSquare.Tick", time}) 23 | 24 | export const step/*:type.step*/ = (model, action) => { 25 | if (action.type === "SpinSquare.Spin") { 26 | if (model.animationState == null) { 27 | return [model, Effects.tick(asTick)] 28 | } else { 29 | return [model, Effects.none] 30 | } 31 | } 32 | 33 | if (action.type === "SpinSquare.Tick") { 34 | const {animationState, angle} = model 35 | const elapsedTime = animationState == null ? 36 | 0 : 37 | animationState.elapsedTime + (action.time - animationState.lastTime) 38 | 39 | if (elapsedTime > duration) { 40 | return [ 41 | create({angle: angle + rotateStep, animationState: null}), 42 | Effects.none 43 | ] 44 | } else { 45 | return [ 46 | create({angle, animationState: {elapsedTime, lastTime: action.time}}), 47 | Effects.tick(asTick) 48 | ] 49 | } 50 | } 51 | 52 | 53 | return [model, Effects.none] 54 | } 55 | 56 | // View 57 | 58 | const toOffset = animationState => 59 | animationState == null ? 60 | 0 : 61 | ease(easeOutBounce, float, 0, 62 | rotateStep, duration, animationState.elapsedTime) 63 | 64 | 65 | const style = { 66 | square: { 67 | width: "200px", 68 | height: "200px", 69 | display: "flex", 70 | alignItems: "center", 71 | justifyContent: "center", 72 | backgroundColor: "#60B5CC", 73 | color: "#fff", 74 | cursor: "pointer", 75 | borderRadius: "30px" 76 | }, 77 | spin({angle, animationState}) { 78 | return { 79 | transform: `translate(100px, 100px) rotate(${angle + toOffset(animationState)}deg)` 80 | } 81 | } 82 | } 83 | 84 | export const view/*:type.view*/ = (model, address) => 85 | html.figure({ 86 | key: "spin-square", 87 | style: Object.assign({}, style.spin(model), style.square), 88 | onClick: forward(address, asSpin) 89 | }, ["Click me!"]) 90 | -------------------------------------------------------------------------------- /examples/spin-squares/type/spin-square-pair.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | 4 | import type {Address, VirtualNode} from "reflex/type" 5 | import type {Effects} from "reflex/type/effects" 6 | import * as SpinSquare from "./spin-square" 7 | 8 | export type State = { 9 | left: SpinSquare.State, 10 | right: SpinSquare.State 11 | } 12 | 13 | export type Model = { 14 | left: SpinSquare.Model, 15 | right: SpinSquare.Model 16 | } 17 | 18 | 19 | export type Left = { 20 | type: "SpinSquarePair.Left", 21 | act: SpinSquare.Action 22 | } 23 | 24 | export type Right = { 25 | type: "SpinSquarePair.Right", 26 | act: any // Workaround for facebook/flow#953 27 | // act: SpinSquare.Action 28 | } 29 | 30 | export type Action 31 | = Left 32 | | Right 33 | 34 | 35 | export type asLeft = (action:SpinSquare.Action) => Left 36 | export type asRight = (action:SpinSquare.Action) => Right 37 | 38 | export type create = (options:State) => Model 39 | export type initialize = () => [Model, Effects] 40 | 41 | export type step = (model:Model, action:Action) => [Model, Effects] 42 | 43 | export type view = (model:Model, address:Address) => VirtualNode 44 | -------------------------------------------------------------------------------- /examples/spin-squares/type/spin-square.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type {Float, Time} from "eased/type" 4 | import type {Address, VirtualNode} from "reflex/type" 5 | import type {Effects} from "reflex/type/effects" 6 | 7 | 8 | export type AnimationState = { 9 | lastTime: Time, 10 | elapsedTime: Time 11 | } 12 | 13 | export type State = { 14 | angle: Float, 15 | animationState: ?AnimationState 16 | } 17 | 18 | export type Model 19 | = {type: "SpinSquare.Model"} 20 | & State 21 | 22 | export type Spin = {type: "SpinSquare.Spin"} 23 | export type Tick = {type: "SpinSquare.Tick", time: Time} 24 | export type Action 25 | = Spin 26 | | Tick 27 | 28 | export type asSpin = () => Spin 29 | export type asTick = (time:Time) => Tick 30 | 31 | export type create = (options:State) => Model 32 | export type initialize = () => [Model, Effects] 33 | 34 | export type step = (model:Model, action:Action) => [Model, Effects] 35 | 36 | export type view = (model:Model, address:Address) => VirtualNode 37 | -------------------------------------------------------------------------------- /interfaces/dom.js: -------------------------------------------------------------------------------- 1 | /*flow*/ 2 | declare function requestAnimationFrame(callback: any): number; 3 | declare class performance { 4 | static now(): number; 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reflex-react-driver", 3 | "version": "0.0.3", 4 | "description": "React based renderer for reflex", 5 | "keywords": [ 6 | "reflex", 7 | "react", 8 | "renderer" 9 | ], 10 | "author": "Irakli Gozalishvili (http://jeditoolkit.com)", 11 | "homepage": "https://github.com/Gozala/reflex-react-driver", 12 | "main": "./lib/index.js", 13 | "dependencies": { 14 | "react": "0.13.3", 15 | "blanks": "0.0.2", 16 | "object-as-dictionary": "0.0.3" 17 | }, 18 | "devDependencies": { 19 | "babel": "5.6.14", 20 | "babel-plugin-flow-comments": "1.0.9", 21 | "flow-bin": "0.17.0", 22 | "reflex": "0.0.50", 23 | "tap": "1.1.0", 24 | "tape": "2.3.2" 25 | }, 26 | "scripts": { 27 | "test": "flow check", 28 | "check": "flow check", 29 | "build-node": "babel ./src --out-dir ./lib --plugins flow-comments --blacklist flow", 30 | "build-browser": "babel ./src --out-dir ./dist --modules umdStrict", 31 | "build": "npm run build-node && npm run build-browser", 32 | "prepublish": "npm run build" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/Gozala/reflex-react-driver.git", 37 | "web": "https://github.com/Gozala/reflex-react-driver" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/Gozala/reflex-react-driver/issues/" 41 | }, 42 | "licenses": [ 43 | { 44 | "type": "MIT", 45 | "url": "https://github.com/Gozala/reflex-react-driver/License.md" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Recat@0.14 uses different API for elemnets, custom value for `$$typeof` 4 | // field is used to signify that instance implement Element interface. 5 | // See: https://github.com/facebook/react/blob/master/src/isomorphic/classic/element/ReactElement.js#L18-L22 6 | export const reactElementType = (typeof(Symbol) === 'function' && Symbol.for != null) ? 7 | Symbol.for('react.element') : 8 | 0xeac7 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | 4 | import * as React from "react"; 5 | import {node} from "./node" 6 | import {thunk} from "./thunk" 7 | 8 | /*:: 9 | import type {VirtualTree, Address, Driver} from "reflex/type"; 10 | 11 | type RenderTarget = Node & Element & HTMLElement 12 | type Configuration = {target: RenderTarget, timeGroupName?:string} 13 | */ 14 | 15 | export class Renderer { 16 | /*:: 17 | target: RenderTarget; 18 | value: Driver.VirtualRoot; 19 | isScheduled: boolean; 20 | version: number; 21 | address: Address; 22 | execute: () => void; 23 | timeGroupName: ?string; 24 | 25 | render: Driver.render; 26 | node: Driver.node; 27 | thunk: Driver.thunk; 28 | text: ?Driver.text; 29 | */ 30 | constructor({target, timeGroupName}/*:Configuration*/) { 31 | this.isScheduled = false 32 | this.version = 0 33 | 34 | this.target = target 35 | this.timeGroupName = timeGroupName == null ? null : timeGroupName 36 | 37 | this.address = this.receive.bind(this) 38 | this.execute = this.execute.bind(this) 39 | } 40 | toString()/*:string*/{ 41 | return `Renderer({target: ${this.target}})` 42 | } 43 | receive(value/*:Driver.VirtualRoot*/) { 44 | this.value = value 45 | this.schedule() 46 | } 47 | schedule() { 48 | if (!this.isScheduled) { 49 | this.isScheduled = true 50 | this.version = requestAnimationFrame(this.execute) 51 | } 52 | } 53 | execute(_/*:number*/) { 54 | const {timeGroupName} = this 55 | if (timeGroupName != null) { 56 | console.time(`render ${timeGroupName}`) 57 | } 58 | 59 | const start = performance.now() 60 | 61 | // It is important to mark `isScheduled` as `false` before doing actual 62 | // rendering since state changes in effect of reflecting current state 63 | // won't be handled by this render cycle. For example rendering a state 64 | // with updated focus will cause `blur` & `focus` events to be dispatched 65 | // that happen synchronously, and there for another render cycle may be 66 | // scheduled for which `isScheduled` must be `false`. Attempt to render 67 | // this state may also cause a runtime exception but even then we would 68 | // rather attempt to render updated states that end up being blocked 69 | // forever. 70 | this.isScheduled = false 71 | 72 | this.value.renderWith(this) 73 | 74 | const end = performance.now() 75 | const time = end - start 76 | 77 | if (time > 16) { 78 | console.warn(`Render took ${time}ms & will cause frame drop`) 79 | } 80 | 81 | if (timeGroupName != null) { 82 | console.time(`render ${timeGroupName}`) 83 | } 84 | } 85 | render(tree/*:VirtualTree*/) { 86 | React.render(tree, this.target) 87 | } 88 | } 89 | Renderer.prototype.text = null 90 | Renderer.prototype.node = node 91 | Renderer.prototype.thunk = thunk 92 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {reactElementType} from "./core" 4 | import {empty} from "blanks/lib/array" 5 | import {blank} from "blanks/lib/object" 6 | 7 | /*:: 8 | import * as type from "../type" 9 | */ 10 | 11 | // VirtualNode implements same interface as result of `React.createElement` 12 | // but it's strictly for DOM elements. For thunks a.k.a components (in react 13 | // vocabulary) we will have a different type. 14 | export class VirtualNode { 15 | /*:: 16 | // Refelx VirtualNode type interface 17 | $type: "VirtualNode"; 18 | key: ?type.Key; 19 | tagName: type.TagName; 20 | namespace: ?string; 21 | children: Array; 22 | 23 | // React 24 | // VirtualNode is exclusively represents virtual HTML nodes as thunks 25 | // a.k.a components (in react vocabulary) have their own type. 26 | type: string; 27 | 28 | // Interface of React@0.13 defines `_isReactElement` to signify that object 29 | // implement element interface. 30 | _isReactElement: boolean; 31 | _store: type.Store; 32 | 33 | // In react@1.4 34 | $$typeof: symbol|number; 35 | props: type.NodeProps; 36 | 37 | // VirtualNode implements interface for several react versions, in order 38 | // to avoid extra allocations instance is also used as `_store` to support 39 | // 0.13 and there for we denfine `originalProps` to comply to the store 40 | // interface. 41 | originalProps: type.NodeProps; 42 | 43 | // Note _owner & ref fields are not used and there for ommited. 44 | */ 45 | constructor(tagName/*:string*/, namespace/*:?string*/, props/*:type.NodeProps*/, children/*:Array*/) { 46 | // reflex 47 | this.tagName = tagName 48 | this.namespace = namespace 49 | this.key = props.key == null ? null : String(props.key) 50 | 51 | const count = children.length 52 | let index = 0 53 | while (index < count) { 54 | const child = children[index] 55 | 56 | 57 | if (typeof(child) === "string") { 58 | // It is important to check for string type first cause there is no 59 | // guarantee that `String.prototype.$type` may 60 | // (see facebook/flow#957) 61 | } else if (child.$type === "VirtualText") { 62 | children[index] = child.text 63 | } else if (child.$type === "LazyTree") { 64 | children[index] = child.force() 65 | index = index - 1 66 | } 67 | 68 | 69 | index = index + 1 70 | } 71 | 72 | this.children = children 73 | 74 | 75 | // React 76 | this.type = tagName 77 | // Unbox single child otherwise react will wrap text nodes into 78 | // spans. 79 | props.children = children.length === 1 ? children[0] : 80 | children; 81 | 82 | // React@0.14 83 | this.props = props 84 | // Use `this` as `_store` to avoid extra allocation. There for 85 | // we define additional `originalProps` to implement `_store` interface. 86 | // We don't actually worry about mutations as our API does not expose 87 | // props to user anyhow. 88 | this._store = this 89 | this.originalProps = props 90 | } 91 | } 92 | // React@0.14 93 | VirtualNode.prototype.$$typeof = reactElementType 94 | // React@0.13 95 | VirtualNode.prototype._isReactElement = true 96 | 97 | export const node/*:type.node*/ = (tagName, properties, children) => 98 | new VirtualNode(tagName, 99 | null, 100 | properties == null ? {children: null} : properties, 101 | children == null ? empty : children) 102 | -------------------------------------------------------------------------------- /src/thunk.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {reactElementType} from "./core" 4 | import {empty} from "blanks/lib/array" 5 | import {blank} from "blanks/lib/object" 6 | 7 | /*:: 8 | import * as type from "../type" 9 | import * as DOM from "reflex/type/dom"; 10 | import * as Driver from "reflex/type/driver" 11 | import type {Address} from "reflex/type/signal"; 12 | 13 | type RenderTarget = Node & Element & HTMLElement; 14 | type Configuration = {target: RenderTarget}; 15 | */ 16 | 17 | 18 | 19 | /*:: 20 | export type View = (...args:Array) => DOM.VirtualTree 21 | type ThunkProps = { 22 | key: string; 23 | view: View; 24 | args: Array; 25 | } 26 | */ 27 | 28 | export class ThunkNode { 29 | /*:: 30 | $type: "Thunk"; 31 | key: type.Key; 32 | 33 | 34 | // Recat 35 | type: NamedThunk; 36 | 37 | // react@1.4 38 | $$typeof: symbol|number; 39 | 40 | // React@10.3 41 | _isReactElement: boolean; 42 | _store: type.Store; 43 | 44 | 45 | props: ThunkProps; 46 | originalProps: ThunkProps; 47 | */ 48 | constructor(key/*:string*/, NamedThunk/*:NamedThunk*/, view/*:View*/, args/*:Array*/) { 49 | const props = {key, view, args} 50 | 51 | // React 52 | this.key = key 53 | this.type = NamedThunk 54 | 55 | // React@0.14 56 | this.$$typeof = reactElementType 57 | this.props = props 58 | 59 | // React@0.13 60 | this._isReactElement = true 61 | this.originalProps = props 62 | this._store = this 63 | } 64 | } 65 | 66 | const redirect = (addressBook, index) => 67 | action => addressBook[index](action); 68 | 69 | // Thunk implements React.Component interface although it's comprised of 70 | // view function and arguments to bassed to it vs a subclassing and passing 71 | // props. It represents subtree of the virtual dom that is computed lazily 72 | // & since computation function and input is packed with this type it provides 73 | // an opportunity to skip computation if two thunks are packagings of same 74 | // view and args. 75 | // Based on: https://github.com/facebook/react/blob/master/src/isomorphic/modern/class/ReactComponent.js 76 | /*:: 77 | type ThunkState = { 78 | args: Array; 79 | addressBook: Array; 80 | }; 81 | type NamedThunk = SubClass; 82 | */ 83 | export class Thunk { 84 | /*:: 85 | // Reflex keeps track of number of mounts as Thunk's are cached 86 | // by a displayName. 87 | static mounts: number; 88 | // Reflex stores currently operating `view` function into `Thunk.context` 89 | // in order to allow memoization functions to be contextual. 90 | static context: ?View; 91 | 92 | // React devtools presents information based on `displayName` 93 | static displayName: string; 94 | 95 | // React component 96 | props: ThunkProps; 97 | state: ThunkState; 98 | 99 | context: any; 100 | refs: any; 101 | updater: any; 102 | */ 103 | constructor(props/*:ThunkProps*/, context/*:any*/, updater/*:any*/) { 104 | this.props = props 105 | this.context = context 106 | this.refs = blank 107 | this.updater = updater 108 | 109 | this.state = {addressBook: [], args: []} 110 | } 111 | static withDisplayName(displayName/*:string*/) { 112 | class NamedThunk extends Thunk { 113 | /*:: 114 | static mounts: number; 115 | */ 116 | } 117 | NamedThunk.displayName = displayName 118 | NamedThunk.mounts = 0 119 | return NamedThunk 120 | } 121 | componentWillMount() { 122 | // Increase number of mounts for this Thunk type. 123 | ++this.constructor.mounts 124 | 125 | const {addressBook, args} = this.state 126 | const {args: input} = this.props 127 | const count = input.length 128 | 129 | let index = 0 130 | while (index < count) { 131 | const arg = input[index] 132 | if (typeof(arg) === 'function') { 133 | addressBook[index] = arg 134 | args[index] = redirect(addressBook, index) 135 | } else { 136 | args[index] = arg 137 | } 138 | index = index + 1 139 | } 140 | } 141 | shouldComponentUpdate(props/*:ThunkProps*/, _/*:ThunkState*/)/*:boolean*/{ 142 | const {key, view, args: passed} = props 143 | 144 | if (profile) { 145 | console.time(`${key}.receive`) 146 | } 147 | 148 | const {args, addressBook} = this.state 149 | 150 | const count = passed.length 151 | let index = 0 152 | let isUpdated = this.props.view !== view; 153 | 154 | if (args.length !== count) { 155 | isUpdated = true 156 | args.length = count 157 | addressBook.length = count 158 | } 159 | 160 | while (index < count) { 161 | const next = passed[index] 162 | const arg = args[index] 163 | 164 | if (next !== arg) { 165 | const isNextAddress = typeof(next) === 'function' 166 | const isCurrentAddress = typeof(arg) === 'function' 167 | 168 | if (isNextAddress && isCurrentAddress) { 169 | // Update adrress book with a new address. 170 | addressBook[index] = next 171 | } else { 172 | isUpdated = true 173 | 174 | if (isNextAddress) { 175 | addressBook[index] = next 176 | args[index] = redirect(addressBook, index) 177 | } else { 178 | args[index] = next 179 | } 180 | } 181 | } 182 | 183 | index = index + 1 184 | } 185 | 186 | if (profile) { 187 | console.timeEnd(`${key}.receive`) 188 | } 189 | 190 | return isUpdated 191 | } 192 | render()/*:DOM.VirtualTree*/ { 193 | if (profile) { 194 | console.time(`${this.props.key}.render`) 195 | } 196 | 197 | const {args} = this.state 198 | const {view, key} = this.props 199 | 200 | // Store current context and change current context to view. 201 | const context = Thunk.context 202 | Thunk.context = view 203 | 204 | const tree = view(...args) 205 | 206 | // Restore previosu context. 207 | Thunk.context = context 208 | 209 | if (profile) { 210 | console.timeEnd(`${key}.render`) 211 | } 212 | 213 | return tree 214 | } 215 | componentWillUnmount() { 216 | // Decrement number of mounts for this Thunk type if no more mounts left 217 | // remove it from the cache map. 218 | if (--this.constructor.mounts === 0) { 219 | delete thunkCacheTable[this.constructor.displayName]; 220 | } 221 | } 222 | 223 | } 224 | 225 | // Following symbol is used for cacheing Thunks by an associated displayName 226 | // under `React.Component[thunks]` namespace. This way we workaround reacts 227 | // remounting behavior if element type does not match (see facebook/react#4826). 228 | export const thunks = (typeof(Symbol) === "function" && Symbol.for != null) ? 229 | Symbol.for("reflex/thunk/0.1") : 230 | "reflex/thunk/0.1"; 231 | 232 | // Alias cache table locally or create new table under designated namespace 233 | // and then alias it. 234 | /*:: 235 | type ThunkTable = {[key: string]: NamedThunk}; 236 | */ 237 | const thunkCacheTable /*:ThunkTable*/ = global[thunks] != null ? global[thunks] : 238 | (global[thunks] = Object.create(null)); 239 | 240 | 241 | let profile 242 | 243 | export const thunk/*:type.thunk*/ = (key, view, ...args) => { 244 | const name = key.split("@")[0]; 245 | const type = thunkCacheTable[name] != null ? thunkCacheTable[name] : 246 | (thunkCacheTable[name] = Thunk.withDisplayName(name)); 247 | 248 | return new ThunkNode(key, type, view, args); 249 | }; 250 | -------------------------------------------------------------------------------- /type/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as Signal from "reflex/type/signal" 4 | import * as VirtualDOM from "reflex/type/dom" 5 | import * as Driver from "reflex/type/driver" 6 | 7 | 8 | export type Store = { 9 | props: Props; 10 | originalProps: Props; 11 | } 12 | 13 | export type NodeChildren 14 | = Array 15 | | VirtualDOM.VirtualTree 16 | 17 | export type NodeProps 18 | = VirtualDOM.PropertyDictionary 19 | & {children: ?NodeChildren} 20 | 21 | 22 | export type Address 23 | = Signal.Address 24 | & {reflexEventListener?: EventHandler} 25 | 26 | export type AddressBook = Signal.AddressBook 27 | export type redirect 28 | = (addressBook:AddressBook, index:number) => Address 29 | 30 | export type Key = VirtualDOM.Key 31 | export type TagName = VirtualDOM.TagName 32 | export type AttributeDictionary = VirtualDOM.AttributeDictionary 33 | export type StyleDictionary = VirtualDOM.StyleDictionary 34 | export type PropertyDictionary = VirtualDOM.PropertyDictionary 35 | export type VirtualNode = VirtualDOM.VirtualNode 36 | export type VirtualText = VirtualDOM.VirtualText 37 | export type Text = VirtualDOM.Text 38 | export type VirtualTree = VirtualDOM.VirtualTree 39 | export type Thunk = VirtualDOM.Thunk 40 | export type View = VirtualDOM.View 41 | export type text = Driver.text 42 | export type node = Driver.node 43 | export type thunk = Driver.thunk 44 | --------------------------------------------------------------------------------