├── .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 |
--------------------------------------------------------------------------------