Merge pull request #3 from Gozala/flowtypes
Update to new reflex & flow versions
This commit is contained in:
Коммит
c0248855bc
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
14
.flowconfig
14
.flowconfig
|
@ -1,15 +1,15 @@
|
|||
[ignore]
|
||||
.*/dist/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/flow-bin/.*
|
||||
.*/examples/.*
|
||||
.*/src/test/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/reflex/dist/.*
|
||||
.*/reflex-virtual-dom-driver/lib/.*
|
||||
.*/reflex-virtual-dom-driver/dist/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/examlpes/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-driver/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-driver' -> 'reflex-virtual-dom-driver/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Counter
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to [demo][]
|
||||
|
||||
|
||||
[demo]:http://localhost:6061/
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
|
@ -16,120 +14,119 @@ import sequencial from 'gulp-sequence';
|
|||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
import manifest from './package.json';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
class Bundler {
|
||||
constructor(options) {
|
||||
this.options = options
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
this.build = this.build.bind(this);
|
||||
this.plugin = []
|
||||
this.transform = []
|
||||
this.cache = {}
|
||||
this.entry = path.join
|
||||
( options.input
|
||||
, options.main
|
||||
)
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
if (options.babel != null) {
|
||||
this.transform.push(babelify)
|
||||
}
|
||||
|
||||
if (options.hotreload != null) {
|
||||
this.plugin.push(hmr)
|
||||
this.transform.push(hotify)
|
||||
}
|
||||
|
||||
this.bundler = browserify
|
||||
( { entries: [this.entry]
|
||||
, debug:
|
||||
( options.sourceMaps == null
|
||||
? false
|
||||
: options.sourceMaps === false
|
||||
? false
|
||||
: true
|
||||
)
|
||||
, cache: this.cache
|
||||
, transform: this.transform
|
||||
, plugin: this.plugin
|
||||
}
|
||||
)
|
||||
|
||||
if (options.watch) {
|
||||
this.watcher =
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build)
|
||||
}
|
||||
}
|
||||
bundle() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
const output =
|
||||
( this.options.watch
|
||||
? this.watcher.bundle()
|
||||
: this.bundler.bundle()
|
||||
)
|
||||
return output
|
||||
}
|
||||
build() {
|
||||
const transforms =
|
||||
[ source(this.options.main)
|
||||
, buffer()
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.init({loadMaps: true})
|
||||
)
|
||||
, ( this.options.compression == null
|
||||
? null
|
||||
: uglify(this.options.compression)
|
||||
)
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.write(this.options.sourceMaps.output)
|
||||
)
|
||||
, gulp.dest(this.options.output)
|
||||
]
|
||||
|
||||
const output =
|
||||
transforms.reduce
|
||||
( (input, transform) =>
|
||||
( transform != null
|
||||
? input.pipe(transform)
|
||||
: input
|
||||
)
|
||||
, this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return output.on('end', () => gutil.log(`Completed bundling: '${this.entry}'`))
|
||||
}
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = config => () => {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
root: path.join(module.filename, config.root),
|
||||
cache: config.cache
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
server.listen(config.port);
|
||||
gutil.log(`Navigate to http://localhost:${config.port}/`)
|
||||
}
|
||||
var bundler = config => () => new Bundler(config).build()
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
Object.keys(manifest.builds).forEach(name => {
|
||||
const config = manifest.builds[name]
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
||||
if (config.server) {
|
||||
gulp.task(`serve ${name}`, server(config.server))
|
||||
gulp.task(`build ${name}`, bundler(config))
|
||||
gulp.task(name, sequencial(`build ${name}`, `serve ${name}`))
|
||||
}
|
||||
else {
|
||||
gulp.task(name, bundler(config))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
<main>
|
||||
</main>
|
||||
<script src="./dist/virtual-dom-driver.js"></script>
|
||||
<scrit src="./dist/reflex-driver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
{
|
||||
"name": "counter-list",
|
||||
"description": "Reflex counter list example",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
"start": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-driver": "latest"
|
||||
"reflex": "0.2.0",
|
||||
"reflex-virtual-dom-driver": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"babel-core": "6.7.2",
|
||||
"babel-plugin-remove-comments": "2.0.0",
|
||||
"babel-plugin-syntax-flow": "6.3.13",
|
||||
"babel-plugin-transform-es2015-modules-umd": "6.4.3",
|
||||
"babel-plugin-transform-flow-strip-types": "6.4.0",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babelify": "7.2.0",
|
||||
"browserify": "13.0.0",
|
||||
"browserify-hmr": "0.3.1",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"flow-bin": "0.22.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-sequence": "0.4.5",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"gulp-uglify": "1.5.3",
|
||||
"gulp-util": "3.0.7",
|
||||
"hotify": "0.1.0",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
"builds": {
|
||||
"default": {
|
||||
"//compression": {
|
||||
"mangle": true,
|
||||
"compress": true,
|
||||
"acorn": true
|
||||
},
|
||||
"babel": true,
|
||||
"hotreload": true,
|
||||
"watch": true,
|
||||
"server": {
|
||||
"port": 6061,
|
||||
"cache": 0,
|
||||
"root": "../"
|
||||
},
|
||||
"sourceMaps": {
|
||||
"output": "./"
|
||||
},
|
||||
"main": "./virtual-dom-driver.js",
|
||||
"input": "./src/",
|
||||
"output": "./dist/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,72 +4,134 @@ import * as Counter from "./counter";
|
|||
import {html, forward, thunk} from "reflex";
|
||||
|
||||
/*::
|
||||
import * as type from "../type/counter-list"
|
||||
import type {ID, Model, Action} from "./counter-list"
|
||||
import type {Address, DOM} from "reflex";
|
||||
*/
|
||||
|
||||
export const asAdd/*:type.asAdd*/ = () => ({type: "CounterList.Add"})
|
||||
export const asRemove/*:type.asRemove*/ = () => ({type: "CounterList.Remove"})
|
||||
export const asBy/*:type.asBy*/ = id => act =>
|
||||
({type: "CounterList.ModifyByID", id, act})
|
||||
const Add =
|
||||
() =>
|
||||
( { type: "Add"
|
||||
}
|
||||
)
|
||||
|
||||
const Remove =
|
||||
() =>
|
||||
( { type: "Remove"
|
||||
}
|
||||
)
|
||||
|
||||
export const create/*:type.create*/ = ({nextID, entries}) =>
|
||||
({type: "CounterList.Model", nextID, entries})
|
||||
const Modify =
|
||||
(name/*:ID*/) =>
|
||||
(action/*:Counter.Action*/)/*:Action*/ =>
|
||||
( { type: "Modify"
|
||||
, source: {name, action}
|
||||
}
|
||||
)
|
||||
|
||||
export const add/*:type.add*/ = model => create({
|
||||
nextID: model.nextID + 1,
|
||||
entries: model.entries.concat([{
|
||||
type: "CounterList.Entry",
|
||||
id: model.nextID,
|
||||
model: Counter.create({value: 0})
|
||||
}])
|
||||
})
|
||||
export const init =
|
||||
()/*:Model*/ =>
|
||||
( { nextID: 1
|
||||
, counters: []
|
||||
}
|
||||
)
|
||||
|
||||
export const remove/*:type.remove*/ = model => create({
|
||||
nextID: model.nextID,
|
||||
entries: model.entries.slice(1)
|
||||
})
|
||||
const add =
|
||||
model =>
|
||||
( { nextID: model.nextID + 1
|
||||
, counters:
|
||||
[ ...model.counters
|
||||
, { name: model.nextID
|
||||
, counter: Counter.init(0)
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
export const modify/*:type.modify*/ = (model, id, action) => create({
|
||||
nextID: model.nextID,
|
||||
entries: model.entries.map(entry =>
|
||||
entry.id !== id ?
|
||||
entry :
|
||||
{type: entry.type, id: id, model: Counter.update(entry.model, action)})
|
||||
})
|
||||
const remove =
|
||||
model =>
|
||||
( { nextID: model.nextID
|
||||
, counters: model.counters.slice(1)
|
||||
}
|
||||
)
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "CounterList.Add" ?
|
||||
add(model, action) :
|
||||
action.type === "CounterList.Remove" ?
|
||||
remove(model, action) :
|
||||
action.type === "CounterList.ModifyByID" ?
|
||||
modify(model, action.id, action.act) :
|
||||
model;
|
||||
const modify =
|
||||
(model, {name, action}) =>
|
||||
( { nextID: model.nextID
|
||||
, counters:
|
||||
model.counters.map
|
||||
( ( named ) =>
|
||||
( named.name === name
|
||||
? { name
|
||||
, counter: Counter.update(named.counter, action)
|
||||
}
|
||||
: named
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const update =
|
||||
(model/*:Model*/, action/*:Action*/)/*:Model*/ =>
|
||||
( action.type === "Add"
|
||||
? add(model)
|
||||
: action.type === "Remove"
|
||||
? remove(model)
|
||||
: action.type === "Modify"
|
||||
? modify(model, action.source)
|
||||
: model
|
||||
);
|
||||
|
||||
|
||||
// View
|
||||
const viewEntry/*:type.viewEntry*/ = ({id, model}, address) =>
|
||||
html.div({key: id}, [
|
||||
Counter.view(model, forward(address, asBy(id)))
|
||||
])
|
||||
const viewNamed =
|
||||
(model, address) =>
|
||||
thunk
|
||||
( String(model.name)
|
||||
, renderNamed
|
||||
, model
|
||||
, address
|
||||
)
|
||||
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "CounterList"}, [
|
||||
html.div({key: "controls"}, [
|
||||
html.button({
|
||||
key: "remove",
|
||||
onClick: forward(address, asRemove)
|
||||
}, ["Remove"]),
|
||||
html.button({
|
||||
key: "add",
|
||||
onClick: forward(address, asAdd)
|
||||
}, ["Add"])
|
||||
]),
|
||||
html.div({
|
||||
key: "entries"
|
||||
}, model.entries.map(entry => thunk(String(entry.id),
|
||||
viewEntry,
|
||||
entry,
|
||||
address)))
|
||||
])
|
||||
const renderNamed =
|
||||
(model, address) =>
|
||||
html.div
|
||||
( { key: model.name
|
||||
}
|
||||
, [ Counter.view
|
||||
( model.counter
|
||||
, forward(address, Modify(model.name))
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
export const view =
|
||||
(model/*:Model*/, address/*:Address<Action>*/)/*:DOM*/ =>
|
||||
html.div
|
||||
( { key: "CounterList"
|
||||
}
|
||||
, [ html.div
|
||||
( { key: "controls"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "add"
|
||||
, onClick: forward(address, Add)
|
||||
}
|
||||
, ["Add"]
|
||||
)
|
||||
, html.button
|
||||
( { key: "remove"
|
||||
, onClick: forward(address, Remove)
|
||||
}
|
||||
, ["Remove"]
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "counters"
|
||||
}
|
||||
, model.counters.map
|
||||
( named => viewNamed(named, address)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
import * as Counter from "./counter"
|
||||
|
||||
export type ID = number
|
||||
|
||||
export type NamedCounter =
|
||||
{ name: ID
|
||||
, counter: Counter.Model
|
||||
}
|
||||
|
||||
export type Model =
|
||||
{ nextID: ID
|
||||
, counters: Array<NamedCounter>
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "Add" }
|
||||
| { type: "Remove" }
|
||||
| { type: "Modify"
|
||||
, source:
|
||||
{ name: ID
|
||||
, action: Counter.Action
|
||||
}
|
||||
}
|
||||
|
||||
declare export function Modify
|
||||
( name:ID
|
||||
, action:Counter.Action
|
||||
):Action
|
||||
|
||||
|
||||
declare export function init (): Model
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,45 +1,70 @@
|
|||
/* @flow */
|
||||
import {html, forward} from "reflex";
|
||||
|
||||
import {html, forward} from "reflex"
|
||||
|
||||
/*::
|
||||
import * as type from "../type/counter"
|
||||
import type {Model, Action} from "./counter"
|
||||
import type {Address, DOM} from "reflex"
|
||||
*/
|
||||
|
||||
export const asIncrement/*:type.asIncrement*/ = () =>
|
||||
({type: "Counter.Increment"})
|
||||
export const asDecrement/*:type.asDecrement*/ = () =>
|
||||
({type: "Counter.Decrement"})
|
||||
const Increment =
|
||||
() =>
|
||||
( { type: "Increment"
|
||||
}
|
||||
)
|
||||
|
||||
const Decrement =
|
||||
() =>
|
||||
( { type: "Decrement"
|
||||
}
|
||||
)
|
||||
|
||||
export const create/*:type.create*/ = ({value}) =>
|
||||
({type: "Counter.Model", value})
|
||||
export const init =
|
||||
(value/*:number*/)/*:Model*/ =>
|
||||
({ value })
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "Counter.Increment" ?
|
||||
{type:model.type, value: model.value + 1} :
|
||||
action.type === "Counter.Decrement" ?
|
||||
{type:model.type, value: model.value - 1} :
|
||||
model
|
||||
export const update =
|
||||
( model/*:Model*/
|
||||
, action/*:Action*/
|
||||
)/*:Model*/ =>
|
||||
( action.type === "Increment"
|
||||
? { value: model.value + 1 }
|
||||
: action.type === "Decrement"
|
||||
? { value: model.value - 1 }
|
||||
: model
|
||||
)
|
||||
|
||||
const counterStyle = {
|
||||
value: {
|
||||
fontWeight: "bold"
|
||||
const counterStyle =
|
||||
{ value:
|
||||
{ fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.span({key: "counter"}, [
|
||||
html.button({
|
||||
key: "decrement",
|
||||
onClick: forward(address, asDecrement)
|
||||
}, ["-"]),
|
||||
html.span({
|
||||
key: "value",
|
||||
style: counterStyle.value,
|
||||
}, [String(model.value)]),
|
||||
html.button({
|
||||
key: "increment",
|
||||
onClick: forward(address, asIncrement)
|
||||
}, ["+"])
|
||||
])
|
||||
|
||||
export const view =
|
||||
( model/*:Model*/
|
||||
, address/*:Address<Action>*/
|
||||
)/*:DOM*/ =>
|
||||
html.span
|
||||
( { key: "counter"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "decrement"
|
||||
, onClick: forward(address, Decrement)
|
||||
}
|
||||
, ["-"]
|
||||
)
|
||||
, html.span
|
||||
( { key: "value"
|
||||
, style: counterStyle.value
|
||||
}
|
||||
, [ `${model.value}` ]
|
||||
)
|
||||
, html.button
|
||||
( { key: "increment"
|
||||
, onClick: forward(address, Increment)
|
||||
}
|
||||
, ["+"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
|
||||
export type Model =
|
||||
{ value: number
|
||||
}
|
||||
|
||||
export type Action
|
||||
= {type: "Increment"}
|
||||
| {type: "Decrement"}
|
||||
|
||||
|
||||
declare export function init (value:number):
|
||||
Model
|
||||
|
||||
declare export function update (model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view (model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,18 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as CounterList from "./counter-list"
|
||||
import {start} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
|
||||
const app = start({
|
||||
initial: CounterList.create(window.app != null ?
|
||||
window.app.model.value :
|
||||
{nextID: 0, entries: []}),
|
||||
update: CounterList.update,
|
||||
view: CounterList.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
const renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {init, update, view} from "./counter-list"
|
||||
import {start, beginner} from "reflex"
|
||||
|
||||
export const app = start
|
||||
( beginner
|
||||
( { model:
|
||||
( window.app == null
|
||||
? init()
|
||||
: window.app.model.value
|
||||
)
|
||||
, update: update
|
||||
, view: view
|
||||
}
|
||||
)
|
||||
)
|
||||
window.app = app
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {app} from "./main"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
const renderer = new Renderer
|
||||
( { target: document.body
|
||||
}
|
||||
)
|
||||
|
||||
app.view.subscribe
|
||||
( renderer.address
|
||||
)
|
||||
|
||||
app.task.subscribe
|
||||
( Effects.driver(app.address)
|
||||
)
|
|
@ -1,39 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
import * as Counter from "./counter"
|
||||
|
||||
export type ID = number;
|
||||
export type Entry = {
|
||||
type: "CounterList.Entry",
|
||||
id: ID,
|
||||
model: Counter.Model
|
||||
};
|
||||
|
||||
export type Model = {
|
||||
type: "CounterList.Model",
|
||||
nextID: ID,
|
||||
entries: Array<Entry>
|
||||
};
|
||||
|
||||
|
||||
export type Add = {type: "CounterList.Add"}
|
||||
export type Remove = {type: "CounterList.Remove"}
|
||||
export type ModifyByID = {type: "CounterList.ModifyByID",
|
||||
id:ID,
|
||||
act:Counter.Action}
|
||||
export type Action = Add|Remove|ModifyByID
|
||||
|
||||
|
||||
export type asAdd = () => Add
|
||||
export type asRemove = () => Remove
|
||||
export type asBy = (id:ID) => (act:Counter.Action) => ModifyByID
|
||||
|
||||
export type create = (options:{nextID:ID, entries:Array<Entry>}) => Model
|
||||
export type add = (model:Model) => Model
|
||||
export type remove = (model:Model) => Model
|
||||
export type modify = (model:Model, id:ID, action:Counter.Action) => Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
|
||||
export type viewEntry = (entry:Entry, address:Address<Action>) => VirtualNode
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -1,15 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
|
||||
export type Model = {type: "Counter.Model", value:number}
|
||||
export type Increment = {type: "Counter.Increment"}
|
||||
export type Decrement = {type: "Counter.Decrement"}
|
||||
export type Action = Increment|Decrement
|
||||
|
||||
export type asIncrement = () => Increment
|
||||
export type asDecrement = () => Decrement
|
||||
|
||||
export type create = (options:{value:number}) => Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/examlpes/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-driver/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-driver' -> 'reflex-virtual-dom-driver/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Counter
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to [demo][]
|
||||
|
||||
|
||||
[demo]:http://localhost:6061/
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
|
@ -16,120 +14,119 @@ import sequencial from 'gulp-sequence';
|
|||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
import manifest from './package.json';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
class Bundler {
|
||||
constructor(options) {
|
||||
this.options = options
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
this.build = this.build.bind(this);
|
||||
this.plugin = []
|
||||
this.transform = []
|
||||
this.cache = {}
|
||||
this.entry = path.join
|
||||
( options.input
|
||||
, options.main
|
||||
)
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
if (options.babel != null) {
|
||||
this.transform.push(babelify)
|
||||
}
|
||||
|
||||
if (options.hotreload != null) {
|
||||
this.plugin.push(hmr)
|
||||
this.transform.push(hotify)
|
||||
}
|
||||
|
||||
this.bundler = browserify
|
||||
( { entries: [this.entry]
|
||||
, debug:
|
||||
( options.sourceMaps == null
|
||||
? false
|
||||
: options.sourceMaps === false
|
||||
? false
|
||||
: true
|
||||
)
|
||||
, cache: this.cache
|
||||
, transform: this.transform
|
||||
, plugin: this.plugin
|
||||
}
|
||||
)
|
||||
|
||||
if (options.watch) {
|
||||
this.watcher =
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build)
|
||||
}
|
||||
}
|
||||
bundle() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
const output =
|
||||
( this.options.watch
|
||||
? this.watcher.bundle()
|
||||
: this.bundler.bundle()
|
||||
)
|
||||
return output
|
||||
}
|
||||
build() {
|
||||
const transforms =
|
||||
[ source(this.options.main)
|
||||
, buffer()
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.init({loadMaps: true})
|
||||
)
|
||||
, ( this.options.compression == null
|
||||
? null
|
||||
: uglify(this.options.compression)
|
||||
)
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.write(this.options.sourceMaps.output)
|
||||
)
|
||||
, gulp.dest(this.options.output)
|
||||
]
|
||||
|
||||
const output =
|
||||
transforms.reduce
|
||||
( (input, transform) =>
|
||||
( transform != null
|
||||
? input.pipe(transform)
|
||||
: input
|
||||
)
|
||||
, this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return output.on('end', () => gutil.log(`Completed bundling: '${this.entry}'`))
|
||||
}
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = config => () => {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
root: path.join(module.filename, config.root),
|
||||
cache: config.cache
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
server.listen(config.port);
|
||||
gutil.log(`Navigate to http://localhost:${config.port}/`)
|
||||
}
|
||||
var bundler = config => () => new Bundler(config).build()
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
Object.keys(manifest.builds).forEach(name => {
|
||||
const config = manifest.builds[name]
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
||||
if (config.server) {
|
||||
gulp.task(`serve ${name}`, server(config.server))
|
||||
gulp.task(`build ${name}`, bundler(config))
|
||||
gulp.task(name, sequencial(`build ${name}`, `serve ${name}`))
|
||||
}
|
||||
else {
|
||||
gulp.task(name, bundler(config))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
<main>
|
||||
</main>
|
||||
<script src="./dist/virtual-dom-driver.js"></script>
|
||||
<scrit src="./dist/reflex-driver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
{
|
||||
"name": "counter-pair",
|
||||
"description": "Reflex counter pair example",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
"start": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-driver": "latest"
|
||||
"reflex": "0.2.0",
|
||||
"reflex-virtual-dom-driver": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"babel-core": "6.7.2",
|
||||
"babel-plugin-remove-comments": "2.0.0",
|
||||
"babel-plugin-syntax-flow": "6.3.13",
|
||||
"babel-plugin-transform-es2015-modules-umd": "6.4.3",
|
||||
"babel-plugin-transform-flow-strip-types": "6.4.0",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babelify": "7.2.0",
|
||||
"browserify": "13.0.0",
|
||||
"browserify-hmr": "0.3.1",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"flow-bin": "0.22.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-sequence": "0.4.5",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"gulp-uglify": "1.5.3",
|
||||
"gulp-util": "3.0.7",
|
||||
"hotify": "0.1.0",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
"builds": {
|
||||
"default": {
|
||||
"//compression": {
|
||||
"mangle": true,
|
||||
"compress": true,
|
||||
"acorn": true
|
||||
},
|
||||
"babel": true,
|
||||
"hotreload": true,
|
||||
"watch": true,
|
||||
"server": {
|
||||
"port": 6061,
|
||||
"cache": 0,
|
||||
"root": "../"
|
||||
},
|
||||
"sourceMaps": {
|
||||
"output": "./"
|
||||
},
|
||||
"main": "./virtual-dom-driver.js",
|
||||
"input": "./src/",
|
||||
"output": "./dist/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,55 +4,89 @@ import * as Counter from "./counter";
|
|||
import {html, forward} from "reflex";
|
||||
|
||||
/*::
|
||||
import type {VirtualNode, Address} from "reflex/type"
|
||||
import * as type from "../type/counter-pair"
|
||||
import type {Address, DOM} from "reflex"
|
||||
import type {Action, Model} from "./counter-pair"
|
||||
*/
|
||||
|
||||
|
||||
export const asTop/*:type.asTop*/ = act =>
|
||||
({type: "CounterPair.Top", act})
|
||||
const Top =
|
||||
(action) =>
|
||||
( { type: "Top"
|
||||
, source: action
|
||||
}
|
||||
)
|
||||
|
||||
export const asBottom/*:type.asBottom*/ = act =>
|
||||
({type: "CounterPair.Bottom", act})
|
||||
const Bottom =
|
||||
(action) =>
|
||||
( { type: "Bottom"
|
||||
, source: action
|
||||
}
|
||||
)
|
||||
|
||||
export const asReset/*:type.asReset*/ = () =>
|
||||
({type: "CounterPair.Reset"})
|
||||
const Reset =
|
||||
() =>
|
||||
( { type: "Reset"
|
||||
}
|
||||
)
|
||||
|
||||
export const init =
|
||||
(top/*:number*/, bottom/*:number*/)/*:Model*/ =>
|
||||
( { top: Counter.init(top)
|
||||
, bottom: Counter.init(bottom)
|
||||
}
|
||||
)
|
||||
|
||||
export const update =
|
||||
(model/*:Model*/, action/*:Action*/)/*:Model*/ =>
|
||||
( action.type === "Top"
|
||||
? { top: Counter.update(model.top, action.source)
|
||||
, bottom: model.bottom
|
||||
}
|
||||
: action.type === "Bottom"
|
||||
? { top: model.top
|
||||
, bottom: Counter.update(model.bottom, action.source)
|
||||
}
|
||||
: action.type === "Reset"
|
||||
? init(0, 0)
|
||||
: model
|
||||
)
|
||||
|
||||
|
||||
export const create/*:type.create*/ = ({top, bottom}) => ({
|
||||
type: "CounterPair.Model",
|
||||
top: Counter.create(top),
|
||||
bottom: Counter.create(bottom)
|
||||
})
|
||||
|
||||
// Note last two functions are wrapped in too many parenthesis with type
|
||||
// casting comments at the end due to a bug in type checker: facebook/flow#953
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "CounterPair.Top" ?
|
||||
create({top: Counter.update(model.top, action.act),
|
||||
bottom: model.bottom}) :
|
||||
action.type === "CounterPair.Bottom" ?
|
||||
create({top: model.top,
|
||||
bottom: Counter.update(model.bottom, action.act)}) :
|
||||
action.type === "CounterPair.Reset" ?
|
||||
create({top: {value: 0},
|
||||
bottom: {value: 0}}) :
|
||||
model
|
||||
|
||||
// View
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "counter-pair"}, [
|
||||
html.div({key: "top"}, [
|
||||
Counter.view(model.top, forward(address, asTop))
|
||||
]),
|
||||
html.div({key: "bottom"}, [
|
||||
Counter.view(model.bottom, forward(address, asBottom)),
|
||||
]),
|
||||
html.div({key: "controls"}, [
|
||||
html.button({
|
||||
key: "reset",
|
||||
onClick: forward(address, asReset)
|
||||
}, ["Reset"])
|
||||
])
|
||||
])
|
||||
export const view =
|
||||
( model/*:Model*/
|
||||
, address/*:Address<Action>*/
|
||||
)/*:DOM*/ =>
|
||||
html.div
|
||||
( { key: "counter-pair"
|
||||
}
|
||||
, [ html.div
|
||||
( { key: "top"
|
||||
}
|
||||
, [ Counter.view
|
||||
( model.top
|
||||
, forward(address, Top)
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "bottom"
|
||||
}
|
||||
, [ Counter.view
|
||||
( model.bottom
|
||||
, forward(address, Bottom)
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "controls"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "reset"
|
||||
, onClick: forward(address, Reset)
|
||||
}
|
||||
, ["Reset"]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
import * as Counter from "./counter"
|
||||
|
||||
export type Model =
|
||||
{ top: Counter.Model
|
||||
, bottom: Counter.Model
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "Top", source: Counter.Action }
|
||||
| { type: "Bottom", source: Counter.Action }
|
||||
| { type: "Reset" }
|
||||
|
||||
declare export function init
|
||||
(top:number, bottom:number):Model
|
||||
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,45 +1,70 @@
|
|||
/* @flow */
|
||||
import {html, forward} from "reflex";
|
||||
|
||||
import {html, forward} from "reflex"
|
||||
|
||||
/*::
|
||||
import * as type from "../type/counter"
|
||||
import type {Model, Action} from "./counter"
|
||||
import type {Address, DOM} from "reflex"
|
||||
*/
|
||||
|
||||
export const asIncrement/*:type.asIncrement*/ = () =>
|
||||
({type: "Counter.Increment"})
|
||||
export const asDecrement/*:type.asDecrement*/ = () =>
|
||||
({type: "Counter.Decrement"})
|
||||
const Increment =
|
||||
() =>
|
||||
( { type: "Increment"
|
||||
}
|
||||
)
|
||||
|
||||
const Decrement =
|
||||
() =>
|
||||
( { type: "Decrement"
|
||||
}
|
||||
)
|
||||
|
||||
export const create/*:type.create*/ = ({value}) =>
|
||||
({type: "Counter.Model", value})
|
||||
export const init =
|
||||
(value/*:number*/)/*:Model*/ =>
|
||||
({ value })
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "Counter.Increment" ?
|
||||
{type:model.type, value: model.value + 1} :
|
||||
action.type === "Counter.Decrement" ?
|
||||
{type:model.type, value: model.value - 1} :
|
||||
model
|
||||
export const update =
|
||||
( model/*:Model*/
|
||||
, action/*:Action*/
|
||||
)/*:Model*/ =>
|
||||
( action.type === "Increment"
|
||||
? { value: model.value + 1 }
|
||||
: action.type === "Decrement"
|
||||
? { value: model.value - 1 }
|
||||
: model
|
||||
)
|
||||
|
||||
const counterStyle = {
|
||||
value: {
|
||||
fontWeight: "bold"
|
||||
const counterStyle =
|
||||
{ value:
|
||||
{ fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.span({key: "counter"}, [
|
||||
html.button({
|
||||
key: "decrement",
|
||||
onClick: forward(address, asDecrement)
|
||||
}, ["-"]),
|
||||
html.span({
|
||||
key: "value",
|
||||
style: counterStyle.value,
|
||||
}, [String(model.value)]),
|
||||
html.button({
|
||||
key: "increment",
|
||||
onClick: forward(address, asIncrement)
|
||||
}, ["+"])
|
||||
])
|
||||
|
||||
export const view =
|
||||
( model/*:Model*/
|
||||
, address/*:Address<Action>*/
|
||||
)/*:DOM*/ =>
|
||||
html.span
|
||||
( { key: "counter"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "decrement"
|
||||
, onClick: forward(address, Decrement)
|
||||
}
|
||||
, ["-"]
|
||||
)
|
||||
, html.span
|
||||
( { key: "value"
|
||||
, style: counterStyle.value
|
||||
}
|
||||
, [ `${model.value}` ]
|
||||
)
|
||||
, html.button
|
||||
( { key: "increment"
|
||||
, onClick: forward(address, Increment)
|
||||
}
|
||||
, ["+"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
|
||||
export type Model =
|
||||
{ value: number
|
||||
}
|
||||
|
||||
export type Action
|
||||
= {type: "Increment"}
|
||||
| {type: "Decrement"}
|
||||
|
||||
|
||||
declare export function init (value:number):
|
||||
Model
|
||||
|
||||
declare export function update (model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view (model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,19 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as CounterPair from "./counter-pair"
|
||||
import {start} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
|
||||
var app = start({
|
||||
initial: CounterPair.create(window.app != null ?
|
||||
window.app.model.value :
|
||||
{top: {value: 0},
|
||||
bottom: {value: 0}}),
|
||||
update: CounterPair.update,
|
||||
view: CounterPair.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
var renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {init, update, view} from "./counter-pair"
|
||||
import {start, beginner} from "reflex"
|
||||
|
||||
export const app = start
|
||||
( beginner
|
||||
( { model:
|
||||
( window.app == null
|
||||
? init(0, 0)
|
||||
: window.app.model.value
|
||||
)
|
||||
, update: update
|
||||
, view: view
|
||||
}
|
||||
)
|
||||
)
|
||||
window.app = app
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {app} from "./main"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
const renderer = new Renderer
|
||||
( { target: document.body
|
||||
}
|
||||
)
|
||||
|
||||
app.view.subscribe
|
||||
( renderer.address
|
||||
)
|
||||
|
||||
app.task.subscribe
|
||||
( Effects.driver(app.address)
|
||||
)
|
|
@ -1,37 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
import * as Counter from "./counter"
|
||||
|
||||
export type Model = {
|
||||
type: "CounterPair.Model",
|
||||
top: Counter.Model,
|
||||
bottom: Counter.Model
|
||||
}
|
||||
|
||||
export type Top = {
|
||||
type: "CounterPair.Top",
|
||||
act: Counter.Action
|
||||
}
|
||||
export type Bottom = {
|
||||
type: "CounterPair.Bottom",
|
||||
act: any // Workaround for facebook/flow#953
|
||||
// act: Counter.Action
|
||||
}
|
||||
export type Reset = {type: "CounterPair.Reset"}
|
||||
export type Action
|
||||
= Top
|
||||
| Bottom
|
||||
| Reset
|
||||
|
||||
export type asTop = (action:Counter.Action) => Top
|
||||
export type asBottom = (action:Counter.Action) => Bottom
|
||||
export type asReset = () => Reset
|
||||
|
||||
|
||||
export type create = (options:{top:{value:number}, bottom:{value:number}}) =>
|
||||
Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
|
||||
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -1,15 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
|
||||
export type Model = {type: "Counter.Model", value:number}
|
||||
export type Increment = {type: "Counter.Increment"}
|
||||
export type Decrement = {type: "Counter.Decrement"}
|
||||
export type Action = Increment|Decrement
|
||||
|
||||
export type asIncrement = () => Increment
|
||||
export type asDecrement = () => Decrement
|
||||
|
||||
export type create = (options:{value:number}) => Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/examlpes/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-driver/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-driver' -> 'reflex-virtual-dom-driver/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Counter
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to [demo][]
|
||||
|
||||
|
||||
[demo]:http://localhost:6061/
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
|
@ -16,120 +14,119 @@ import sequencial from 'gulp-sequence';
|
|||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
import manifest from './package.json';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
class Bundler {
|
||||
constructor(options) {
|
||||
this.options = options
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
this.build = this.build.bind(this);
|
||||
this.plugin = []
|
||||
this.transform = []
|
||||
this.cache = {}
|
||||
this.entry = path.join
|
||||
( options.input
|
||||
, options.main
|
||||
)
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
if (options.babel != null) {
|
||||
this.transform.push(babelify)
|
||||
}
|
||||
|
||||
if (options.hotreload != null) {
|
||||
this.plugin.push(hmr)
|
||||
this.transform.push(hotify)
|
||||
}
|
||||
|
||||
this.bundler = browserify
|
||||
( { entries: [this.entry]
|
||||
, debug:
|
||||
( options.sourceMaps == null
|
||||
? false
|
||||
: options.sourceMaps === false
|
||||
? false
|
||||
: true
|
||||
)
|
||||
, cache: this.cache
|
||||
, transform: this.transform
|
||||
, plugin: this.plugin
|
||||
}
|
||||
)
|
||||
|
||||
if (options.watch) {
|
||||
this.watcher =
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build)
|
||||
}
|
||||
}
|
||||
bundle() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
const output =
|
||||
( this.options.watch
|
||||
? this.watcher.bundle()
|
||||
: this.bundler.bundle()
|
||||
)
|
||||
return output
|
||||
}
|
||||
build() {
|
||||
const transforms =
|
||||
[ source(this.options.main)
|
||||
, buffer()
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.init({loadMaps: true})
|
||||
)
|
||||
, ( this.options.compression == null
|
||||
? null
|
||||
: uglify(this.options.compression)
|
||||
)
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.write(this.options.sourceMaps.output)
|
||||
)
|
||||
, gulp.dest(this.options.output)
|
||||
]
|
||||
|
||||
const output =
|
||||
transforms.reduce
|
||||
( (input, transform) =>
|
||||
( transform != null
|
||||
? input.pipe(transform)
|
||||
: input
|
||||
)
|
||||
, this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return output.on('end', () => gutil.log(`Completed bundling: '${this.entry}'`))
|
||||
}
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = config => () => {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
root: path.join(module.filename, config.root),
|
||||
cache: config.cache
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
server.listen(config.port);
|
||||
gutil.log(`Navigate to http://localhost:${config.port}/`)
|
||||
}
|
||||
var bundler = config => () => new Bundler(config).build()
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
Object.keys(manifest.builds).forEach(name => {
|
||||
const config = manifest.builds[name]
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
||||
if (config.server) {
|
||||
gulp.task(`serve ${name}`, server(config.server))
|
||||
gulp.task(`build ${name}`, bundler(config))
|
||||
gulp.task(name, sequencial(`build ${name}`, `serve ${name}`))
|
||||
}
|
||||
else {
|
||||
gulp.task(name, bundler(config))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
<main>
|
||||
</main>
|
||||
<script src="./dist/virtual-dom-driver.js"></script>
|
||||
<scrit src="./dist/reflex-driver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
{
|
||||
"name": "counter-set",
|
||||
"description": "Reflex counter set example",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
"start": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-driver": "latest"
|
||||
"reflex": "0.2.0",
|
||||
"reflex-virtual-dom-driver": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"babel-core": "6.7.2",
|
||||
"babel-plugin-remove-comments": "2.0.0",
|
||||
"babel-plugin-syntax-flow": "6.3.13",
|
||||
"babel-plugin-transform-es2015-modules-umd": "6.4.3",
|
||||
"babel-plugin-transform-flow-strip-types": "6.4.0",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babelify": "7.2.0",
|
||||
"browserify": "13.0.0",
|
||||
"browserify-hmr": "0.3.1",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"flow-bin": "0.22.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-sequence": "0.4.5",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"gulp-uglify": "1.5.3",
|
||||
"gulp-util": "3.0.7",
|
||||
"hotify": "0.1.0",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
"builds": {
|
||||
"default": {
|
||||
"//compression": {
|
||||
"mangle": true,
|
||||
"compress": true,
|
||||
"acorn": true
|
||||
},
|
||||
"babel": true,
|
||||
"hotreload": true,
|
||||
"watch": true,
|
||||
"server": {
|
||||
"port": 6061,
|
||||
"cache": 0,
|
||||
"root": "../"
|
||||
},
|
||||
"sourceMaps": {
|
||||
"output": "./"
|
||||
},
|
||||
"main": "./virtual-dom-driver.js",
|
||||
"input": "./src/",
|
||||
"output": "./dist/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,72 +4,122 @@ import * as Counter from "./counter";
|
|||
import {html, forward, thunk} from "reflex";
|
||||
|
||||
/*::
|
||||
import * as type from "../type/counter-list"
|
||||
import type {ID, Model, Action} from "./counter-list"
|
||||
import type {Address, DOM} from "reflex";
|
||||
*/
|
||||
|
||||
export const asAdd/*:type.asAdd*/ = () => ({type: "CounterList.Add"})
|
||||
export const asRemove/*:type.asRemove*/ = () => ({type: "CounterList.Remove"})
|
||||
export const asBy/*:type.asBy*/ = id => act =>
|
||||
({type: "CounterList.ModifyByID", id, act})
|
||||
const Add =
|
||||
() =>
|
||||
( { type: "Add"
|
||||
}
|
||||
)
|
||||
|
||||
const Remove =
|
||||
() =>
|
||||
( { type: "Remove"
|
||||
}
|
||||
)
|
||||
|
||||
export const create/*:type.create*/ = ({nextID, entries}) =>
|
||||
({type: "CounterList.Model", nextID, entries})
|
||||
const Modify =
|
||||
(name/*:ID*/) =>
|
||||
(action/*:Counter.Action*/)/*:Action*/ =>
|
||||
( { type: "Modify"
|
||||
, source: {name, action}
|
||||
}
|
||||
)
|
||||
|
||||
export const add/*:type.add*/ = model => create({
|
||||
nextID: model.nextID + 1,
|
||||
entries: model.entries.concat([{
|
||||
type: "CounterList.Entry",
|
||||
id: model.nextID,
|
||||
model: Counter.create({value: 0})
|
||||
}])
|
||||
})
|
||||
export const init =
|
||||
()/*:Model*/ =>
|
||||
( { nextID: 1
|
||||
, counters: []
|
||||
}
|
||||
)
|
||||
|
||||
export const remove/*:type.remove*/ = model => create({
|
||||
nextID: model.nextID,
|
||||
entries: model.entries.slice(1)
|
||||
})
|
||||
const add =
|
||||
model =>
|
||||
( { nextID: model.nextID + 1
|
||||
, counters:
|
||||
[ ...model.counters
|
||||
, { name: model.nextID
|
||||
, counter: Counter.init(0)
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
export const modify/*:type.modify*/ = (model, id, action) => create({
|
||||
nextID: model.nextID,
|
||||
entries: model.entries.map(entry =>
|
||||
entry.id !== id ?
|
||||
entry :
|
||||
{type: entry.type, id: id, model: Counter.update(entry.model, action)})
|
||||
})
|
||||
const remove =
|
||||
model =>
|
||||
( { nextID: model.nextID
|
||||
, counters: model.counters.slice(1)
|
||||
}
|
||||
)
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "CounterList.Add" ?
|
||||
add(model, action) :
|
||||
action.type === "CounterList.Remove" ?
|
||||
remove(model, action) :
|
||||
action.type === "CounterList.ModifyByID" ?
|
||||
modify(model, action.id, action.act) :
|
||||
model;
|
||||
const modify =
|
||||
(model, {name, action}) =>
|
||||
( { nextID: model.nextID
|
||||
, counters:
|
||||
model.counters.map
|
||||
( ( named ) =>
|
||||
( named.name === name
|
||||
? { name
|
||||
, counter: Counter.update(named.counter, action)
|
||||
}
|
||||
: named
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const update =
|
||||
(model/*:Model*/, action/*:Action*/)/*:Model*/ =>
|
||||
( action.type === "Add"
|
||||
? add(model)
|
||||
: action.type === "Remove"
|
||||
? remove(model)
|
||||
: action.type === "Modify"
|
||||
? modify(model, action.source)
|
||||
: model
|
||||
);
|
||||
|
||||
|
||||
// View
|
||||
const viewEntry/*:type.viewEntry*/ = ({id, model}, address) =>
|
||||
html.div({key: id}, [
|
||||
Counter.view(model, forward(address, asBy(id)))
|
||||
])
|
||||
const viewNamed =
|
||||
(model, address) =>
|
||||
thunk
|
||||
( String(model.name)
|
||||
, Counter.view
|
||||
, model.counter
|
||||
, forward(address, Modify(model.name))
|
||||
)
|
||||
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "CounterList"}, [
|
||||
html.div({key: "controls"}, [
|
||||
html.button({
|
||||
key: "remove",
|
||||
onClick: forward(address, asRemove)
|
||||
}, ["Remove"]),
|
||||
html.button({
|
||||
key: "add",
|
||||
onClick: forward(address, asAdd)
|
||||
}, ["Add"])
|
||||
]),
|
||||
html.div({
|
||||
key: "entries"
|
||||
}, model.entries.map(entry => thunk(String(entry.id),
|
||||
viewEntry,
|
||||
entry,
|
||||
address)))
|
||||
])
|
||||
export const view =
|
||||
(model/*:Model*/, address/*:Address<Action>*/)/*:DOM*/ =>
|
||||
html.div
|
||||
( { key: "CounterList"
|
||||
}
|
||||
, [ html.div
|
||||
( { key: "controls"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "add"
|
||||
, onClick: forward(address, Add)
|
||||
}
|
||||
, ["Add"]
|
||||
)
|
||||
, html.button
|
||||
( { key: "remove"
|
||||
, onClick: forward(address, Remove)
|
||||
}
|
||||
, ["Remove"]
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "counters"
|
||||
}
|
||||
, model.counters.map
|
||||
( named => viewNamed(named, address)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
import * as Counter from "./counter"
|
||||
|
||||
export type ID = number
|
||||
|
||||
export type NamedCounter =
|
||||
{ name: ID
|
||||
, counter: Counter.Model
|
||||
}
|
||||
|
||||
export type Model =
|
||||
{ nextID: ID
|
||||
, counters: Array<NamedCounter>
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "Add" }
|
||||
| { type: "Remove" }
|
||||
| { type: "Modify"
|
||||
, source:
|
||||
{ name: ID
|
||||
, action: Counter.Action
|
||||
}
|
||||
}
|
||||
|
||||
declare export function Modify
|
||||
( name:ID
|
||||
, action:Counter.Action
|
||||
):Action
|
||||
|
||||
|
||||
declare export function init (): Model
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -4,45 +4,123 @@ import * as CounterList from "./counter-list";
|
|||
import * as Counter from "./counter";
|
||||
import {html, forward, thunk} from "reflex";
|
||||
|
||||
/*:: import * as type from "../type/counter-set" */
|
||||
/*::
|
||||
import type {ID, Model, Action} from "./counter-set"
|
||||
import type {DOM, Address} from "reflex"
|
||||
*/
|
||||
|
||||
export const create = CounterList.create
|
||||
const Add =
|
||||
() =>
|
||||
( { type: "Add"
|
||||
}
|
||||
)
|
||||
|
||||
export const asRemoveBy/*:type.asRemoveBy*/ = id => () =>
|
||||
({type: "CounterSet.RemoveByID", id})
|
||||
const Remove =
|
||||
(name) =>
|
||||
() =>
|
||||
( { type: "Remove"
|
||||
, source: name
|
||||
}
|
||||
)
|
||||
|
||||
export const removeByID/*:type.removeByID*/ = (model, id) => create({
|
||||
nextID: model.nextID,
|
||||
entries: model.entries.filter(entry => entry.id != id)
|
||||
})
|
||||
const Modify =
|
||||
(name) =>
|
||||
(action) =>
|
||||
( { type: "Modify"
|
||||
, source: {name, action}
|
||||
}
|
||||
)
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "CounterSet.RemoveByID" ?
|
||||
removeByID(model, action.id) :
|
||||
CounterList.update(model, action)
|
||||
const remove =
|
||||
(model/*:Model*/, name/*:ID*/)/*:Model*/ =>
|
||||
( { nextID: model.nextID
|
||||
, counters:
|
||||
model.counters.filter
|
||||
( named => named.name !== name )
|
||||
}
|
||||
)
|
||||
|
||||
const modify =
|
||||
(model, {name, action}) =>
|
||||
( { nextID: model.nextID
|
||||
, counters:
|
||||
model.counters.map
|
||||
( named =>
|
||||
( named.name === name
|
||||
? { name
|
||||
, counter: Counter.update(named.counter, action)
|
||||
}
|
||||
: named
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const viewEntry/*:type.viewEntry*/ = ({id, model}, address) =>
|
||||
html.div({key: id}, [
|
||||
Counter.view(model, forward(address, CounterList.asBy(id))),
|
||||
html.button({
|
||||
key: "remove",
|
||||
onClick: forward(address, asRemoveBy(id))
|
||||
}, ["x"])
|
||||
])
|
||||
export const init = CounterList.init
|
||||
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "CounterList"}, [
|
||||
html.div({key: "controls"}, [
|
||||
html.button({
|
||||
key: "add",
|
||||
onClick: forward(address, CounterList.asAdd)
|
||||
}, ["Add"])
|
||||
]),
|
||||
html.div({
|
||||
key: "entries"
|
||||
}, model.entries.map(entry => thunk(String(entry.id),
|
||||
viewEntry,
|
||||
entry,
|
||||
address)))
|
||||
])
|
||||
export const update =
|
||||
( model/*:Model*/, action/*:Action*/)/*:Model*/ =>
|
||||
( action.type === "Add"
|
||||
? CounterList.update(model, action)
|
||||
: action.type === "Remove"
|
||||
? remove(model, action.source)
|
||||
: action.type === "Modify"
|
||||
? modify(model, action.source)
|
||||
: model
|
||||
)
|
||||
|
||||
const viewNamed =
|
||||
(model, address) =>
|
||||
thunk
|
||||
( String(model.name)
|
||||
, renderNamed
|
||||
, model
|
||||
, address
|
||||
)
|
||||
|
||||
const renderNamed =
|
||||
(model, address) =>
|
||||
html.div
|
||||
( { key: model.name
|
||||
}
|
||||
, [ Counter.view
|
||||
( model.counter
|
||||
, forward(address, Modify(model.name))
|
||||
)
|
||||
, html.button
|
||||
( { key: "remove"
|
||||
, onClick: forward(address, Remove(model.name))
|
||||
}
|
||||
, ["x"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
export const view =
|
||||
( model/*:Model*/
|
||||
, address/*:Address<Action>*/
|
||||
)/*:DOM*/ =>
|
||||
html.div
|
||||
( { key: "CounterList"
|
||||
}
|
||||
, [ html.div
|
||||
( { key: "controls"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "add"
|
||||
, onClick: forward(address, Add)
|
||||
}
|
||||
, ["Add"]
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "counters"
|
||||
}
|
||||
, model.counters.map
|
||||
( named =>
|
||||
viewNamed(named, address)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* @flow */
|
||||
|
||||
import * as Counter from "./counter"
|
||||
import type {Model, ID} from "./counter-list"
|
||||
import type {Address, DOM} from "reflex"
|
||||
|
||||
export type {Model, ID}
|
||||
|
||||
export type Action
|
||||
= { type: "Add" }
|
||||
| { type: "Remove", source: ID }
|
||||
| { type: "Modify"
|
||||
, source: {name: ID, action: Counter.Action}
|
||||
}
|
||||
|
||||
declare export function init
|
||||
():
|
||||
Model
|
||||
|
||||
declare export function update
|
||||
(model:Model, action:Action)
|
||||
:Model
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,45 +1,70 @@
|
|||
/* @flow */
|
||||
import {html, forward} from "reflex";
|
||||
|
||||
import {html, forward} from "reflex"
|
||||
|
||||
/*::
|
||||
import * as type from "../type/counter"
|
||||
import type {Model, Action} from "./counter"
|
||||
import type {Address, DOM} from "reflex"
|
||||
*/
|
||||
|
||||
export const asIncrement/*:type.asIncrement*/ = () =>
|
||||
({type: "Counter.Increment"})
|
||||
export const asDecrement/*:type.asDecrement*/ = () =>
|
||||
({type: "Counter.Decrement"})
|
||||
const Increment =
|
||||
() =>
|
||||
( { type: "Increment"
|
||||
}
|
||||
)
|
||||
|
||||
const Decrement =
|
||||
() =>
|
||||
( { type: "Decrement"
|
||||
}
|
||||
)
|
||||
|
||||
export const create/*:type.create*/ = ({value}) =>
|
||||
({type: "Counter.Model", value})
|
||||
export const init =
|
||||
(value/*:number*/)/*:Model*/ =>
|
||||
({ value })
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "Counter.Increment" ?
|
||||
{type:model.type, value: model.value + 1} :
|
||||
action.type === "Counter.Decrement" ?
|
||||
{type:model.type, value: model.value - 1} :
|
||||
model
|
||||
export const update =
|
||||
( model/*:Model*/
|
||||
, action/*:Action*/
|
||||
)/*:Model*/ =>
|
||||
( action.type === "Increment"
|
||||
? { value: model.value + 1 }
|
||||
: action.type === "Decrement"
|
||||
? { value: model.value - 1 }
|
||||
: model
|
||||
)
|
||||
|
||||
const counterStyle = {
|
||||
value: {
|
||||
fontWeight: "bold"
|
||||
const counterStyle =
|
||||
{ value:
|
||||
{ fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.span({key: "counter"}, [
|
||||
html.button({
|
||||
key: "decrement",
|
||||
onClick: forward(address, asDecrement)
|
||||
}, ["-"]),
|
||||
html.span({
|
||||
key: "value",
|
||||
style: counterStyle.value,
|
||||
}, [String(model.value)]),
|
||||
html.button({
|
||||
key: "increment",
|
||||
onClick: forward(address, asIncrement)
|
||||
}, ["+"])
|
||||
])
|
||||
|
||||
export const view =
|
||||
( model/*:Model*/
|
||||
, address/*:Address<Action>*/
|
||||
)/*:DOM*/ =>
|
||||
html.span
|
||||
( { key: "counter"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "decrement"
|
||||
, onClick: forward(address, Decrement)
|
||||
}
|
||||
, ["-"]
|
||||
)
|
||||
, html.span
|
||||
( { key: "value"
|
||||
, style: counterStyle.value
|
||||
}
|
||||
, [ `${model.value}` ]
|
||||
)
|
||||
, html.button
|
||||
( { key: "increment"
|
||||
, onClick: forward(address, Increment)
|
||||
}
|
||||
, ["+"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
|
||||
export type Model =
|
||||
{ value: number
|
||||
}
|
||||
|
||||
export type Action
|
||||
= {type: "Increment"}
|
||||
| {type: "Decrement"}
|
||||
|
||||
|
||||
declare export function init (value:number):
|
||||
Model
|
||||
|
||||
declare export function update (model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view (model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,18 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as CounterSet from "./counter-set"
|
||||
import {start} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
|
||||
var app = start({
|
||||
initial: CounterSet.create(window.app != null ?
|
||||
window.app.model.value :
|
||||
{nextID: 0, entries: []}),
|
||||
update: CounterSet.update,
|
||||
view: CounterSet.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
var renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {init, update, view} from "./counter-set"
|
||||
import {start, beginner} from "reflex"
|
||||
|
||||
export const app = start
|
||||
( beginner
|
||||
( { model:
|
||||
( window.app == null
|
||||
? init(0)
|
||||
: window.app.model.value
|
||||
)
|
||||
, update: update
|
||||
, view: view
|
||||
}
|
||||
)
|
||||
)
|
||||
window.app = app
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {app} from "./main"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
const renderer = new Renderer
|
||||
( { target: document.body
|
||||
}
|
||||
)
|
||||
|
||||
app.view.subscribe
|
||||
( renderer.address
|
||||
)
|
||||
|
||||
app.task.subscribe
|
||||
( Effects.driver(app.address)
|
||||
)
|
|
@ -1,39 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
import * as Counter from "./counter"
|
||||
|
||||
export type ID = number;
|
||||
export type Entry = {
|
||||
type: "CounterList.Entry",
|
||||
id: ID,
|
||||
model: Counter.Model
|
||||
};
|
||||
|
||||
export type Model = {
|
||||
type: "CounterList.Model",
|
||||
nextID: ID,
|
||||
entries: Array<Entry>
|
||||
};
|
||||
|
||||
|
||||
export type Add = {type: "CounterList.Add"}
|
||||
export type Remove = {type: "CounterList.Remove"}
|
||||
export type ModifyByID = {type: "CounterList.ModifyByID",
|
||||
id:ID,
|
||||
act:Counter.Action}
|
||||
export type Action = Add|Remove|ModifyByID
|
||||
|
||||
|
||||
export type asAdd = () => Add
|
||||
export type asRemove = () => Remove
|
||||
export type asBy = (id:ID) => (act:Counter.Action) => ModifyByID
|
||||
|
||||
export type create = (options:{nextID:ID, entries:Array<Entry>}) => Model
|
||||
export type add = (model:Model) => Model
|
||||
export type remove = (model:Model) => Model
|
||||
export type modify = (model:Model, id:ID, action:Counter.Action) => Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
|
||||
export type viewEntry = (entry:Entry, address:Address<Action>) => VirtualNode
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -1,23 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as Counter from "./counter"
|
||||
import * as CounterList from "./counter-list"
|
||||
import type {Address, VirtualNode} from "reflex/type";
|
||||
|
||||
export type ID = CounterList.ID
|
||||
export type Entry = CounterList.Entry
|
||||
export type Model = CounterList.Model
|
||||
|
||||
export type RemoveByID = {type: "CounterSet.RemoveByID", id:ID}
|
||||
export type Action
|
||||
= RemoveByID
|
||||
| CounterList.Action
|
||||
|
||||
export type asRemoveBy = (id:ID) => () => RemoveByID
|
||||
|
||||
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
export type removeByID = (model:Model, id:ID) => Model
|
||||
|
||||
export type viewEntry = (entry:Entry, address:Address<Action>) => VirtualNode
|
||||
export type view = (entry:Model, address:Address<Action>) => VirtualNode
|
|
@ -1,15 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
|
||||
export type Model = {type: "Counter.Model", value:number}
|
||||
export type Increment = {type: "Counter.Increment"}
|
||||
export type Decrement = {type: "Counter.Decrement"}
|
||||
export type Action = Increment|Decrement
|
||||
|
||||
export type asIncrement = () => Increment
|
||||
export type asDecrement = () => Decrement
|
||||
|
||||
export type create = (options:{value:number}) => Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/examlpes/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-driver/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-driver' -> 'reflex-virtual-dom-driver/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Counter
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to [demo][]
|
||||
|
||||
|
||||
[demo]:http://localhost:6061/
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
|
@ -16,120 +14,119 @@ import sequencial from 'gulp-sequence';
|
|||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
import manifest from './package.json';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
class Bundler {
|
||||
constructor(options) {
|
||||
this.options = options
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
this.build = this.build.bind(this);
|
||||
this.plugin = []
|
||||
this.transform = []
|
||||
this.cache = {}
|
||||
this.entry = path.join
|
||||
( options.input
|
||||
, options.main
|
||||
)
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
if (options.babel != null) {
|
||||
this.transform.push(babelify)
|
||||
}
|
||||
|
||||
if (options.hotreload != null) {
|
||||
this.plugin.push(hmr)
|
||||
this.transform.push(hotify)
|
||||
}
|
||||
|
||||
this.bundler = browserify
|
||||
( { entries: [this.entry]
|
||||
, debug:
|
||||
( options.sourceMaps == null
|
||||
? false
|
||||
: options.sourceMaps === false
|
||||
? false
|
||||
: true
|
||||
)
|
||||
, cache: this.cache
|
||||
, transform: this.transform
|
||||
, plugin: this.plugin
|
||||
}
|
||||
)
|
||||
|
||||
if (options.watch) {
|
||||
this.watcher =
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build)
|
||||
}
|
||||
}
|
||||
bundle() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
const output =
|
||||
( this.options.watch
|
||||
? this.watcher.bundle()
|
||||
: this.bundler.bundle()
|
||||
)
|
||||
return output
|
||||
}
|
||||
build() {
|
||||
const transforms =
|
||||
[ source(this.options.main)
|
||||
, buffer()
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.init({loadMaps: true})
|
||||
)
|
||||
, ( this.options.compression == null
|
||||
? null
|
||||
: uglify(this.options.compression)
|
||||
)
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.write(this.options.sourceMaps.output)
|
||||
)
|
||||
, gulp.dest(this.options.output)
|
||||
]
|
||||
|
||||
const output =
|
||||
transforms.reduce
|
||||
( (input, transform) =>
|
||||
( transform != null
|
||||
? input.pipe(transform)
|
||||
: input
|
||||
)
|
||||
, this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return output.on('end', () => gutil.log(`Completed bundling: '${this.entry}'`))
|
||||
}
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = config => () => {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
root: path.join(module.filename, config.root),
|
||||
cache: config.cache
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
server.listen(config.port);
|
||||
gutil.log(`Navigate to http://localhost:${config.port}/`)
|
||||
}
|
||||
var bundler = config => () => new Bundler(config).build()
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
Object.keys(manifest.builds).forEach(name => {
|
||||
const config = manifest.builds[name]
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
||||
if (config.server) {
|
||||
gulp.task(`serve ${name}`, server(config.server))
|
||||
gulp.task(`build ${name}`, bundler(config))
|
||||
gulp.task(name, sequencial(`build ${name}`, `serve ${name}`))
|
||||
}
|
||||
else {
|
||||
gulp.task(name, bundler(config))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
<main>
|
||||
</main>
|
||||
<script src="./dist/virtual-dom-driver.js"></script>
|
||||
<scrit src="./dist/reflex-driver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
{
|
||||
"name": "counter",
|
||||
"description": "Reflex counter example",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
"start": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-driver": "latest"
|
||||
"reflex": "0.2.0",
|
||||
"reflex-virtual-dom-driver": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"babel-core": "6.7.2",
|
||||
"babel-plugin-remove-comments": "2.0.0",
|
||||
"babel-plugin-syntax-flow": "6.3.13",
|
||||
"babel-plugin-transform-es2015-modules-umd": "6.4.3",
|
||||
"babel-plugin-transform-flow-strip-types": "6.4.0",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babelify": "7.2.0",
|
||||
"browserify": "13.0.0",
|
||||
"browserify-hmr": "0.3.1",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"flow-bin": "0.22.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-sequence": "0.4.5",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"gulp-uglify": "1.5.3",
|
||||
"gulp-util": "3.0.7",
|
||||
"hotify": "0.1.0",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
"builds": {
|
||||
"default": {
|
||||
"//compression": {
|
||||
"mangle": true,
|
||||
"compress": true,
|
||||
"acorn": true
|
||||
},
|
||||
"babel": true,
|
||||
"hotreload": true,
|
||||
"watch": true,
|
||||
"server": {
|
||||
"port": 6061,
|
||||
"cache": 0,
|
||||
"root": "../"
|
||||
},
|
||||
"sourceMaps": {
|
||||
"output": "./"
|
||||
},
|
||||
"main": "./virtual-dom-driver.js",
|
||||
"input": "./src/",
|
||||
"output": "./dist/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,70 @@
|
|||
/* @flow */
|
||||
import {html, forward} from "reflex";
|
||||
|
||||
import {html, forward} from "reflex"
|
||||
|
||||
/*::
|
||||
import * as type from "../type/counter"
|
||||
import type {Model, Action} from "./counter"
|
||||
import type {Address, DOM} from "reflex"
|
||||
*/
|
||||
|
||||
export const asIncrement/*:type.asIncrement*/ = () =>
|
||||
({type: "Counter.Increment"})
|
||||
export const asDecrement/*:type.asDecrement*/ = () =>
|
||||
({type: "Counter.Decrement"})
|
||||
const Increment =
|
||||
() =>
|
||||
( { type: "Increment"
|
||||
}
|
||||
)
|
||||
|
||||
const Decrement =
|
||||
() =>
|
||||
( { type: "Decrement"
|
||||
}
|
||||
)
|
||||
|
||||
export const create/*:type.create*/ = ({value}) =>
|
||||
({type: "Counter.Model", value})
|
||||
export const init =
|
||||
(value/*:number*/)/*:Model*/ =>
|
||||
({ value })
|
||||
|
||||
export const update/*:type.update*/ = (model, action) =>
|
||||
action.type === "Counter.Increment" ?
|
||||
{type:model.type, value: model.value + 1} :
|
||||
action.type === "Counter.Decrement" ?
|
||||
{type:model.type, value: model.value - 1} :
|
||||
model
|
||||
export const update =
|
||||
( model/*:Model*/
|
||||
, action/*:Action*/
|
||||
)/*:Model*/ =>
|
||||
( action.type === "Increment"
|
||||
? { value: model.value + 1 }
|
||||
: action.type === "Decrement"
|
||||
? { value: model.value - 1 }
|
||||
: model
|
||||
)
|
||||
|
||||
const counterStyle = {
|
||||
value: {
|
||||
fontWeight: "bold"
|
||||
const counterStyle =
|
||||
{ value:
|
||||
{ fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.span({key: "counter"}, [
|
||||
html.button({
|
||||
key: "decrement",
|
||||
onClick: forward(address, asDecrement)
|
||||
}, ["-"]),
|
||||
html.span({
|
||||
key: "value",
|
||||
style: counterStyle.value,
|
||||
}, [String(model.value)]),
|
||||
html.button({
|
||||
key: "increment",
|
||||
onClick: forward(address, asIncrement)
|
||||
}, ["+"])
|
||||
])
|
||||
|
||||
export const view =
|
||||
( model/*:Model*/
|
||||
, address/*:Address<Action>*/
|
||||
)/*:DOM*/ =>
|
||||
html.span
|
||||
( { key: "counter"
|
||||
}
|
||||
, [ html.button
|
||||
( { key: "decrement"
|
||||
, onClick: forward(address, Decrement)
|
||||
}
|
||||
, ["-"]
|
||||
)
|
||||
, html.span
|
||||
( { key: "value"
|
||||
, style: counterStyle.value
|
||||
}
|
||||
, [ `${model.value}` ]
|
||||
)
|
||||
, html.button
|
||||
( { key: "increment"
|
||||
, onClick: forward(address, Increment)
|
||||
}
|
||||
, ["+"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, DOM} from "reflex"
|
||||
|
||||
export type Model =
|
||||
{ value: number
|
||||
}
|
||||
|
||||
export type Action
|
||||
= {type: "Increment"}
|
||||
| {type: "Decrement"}
|
||||
|
||||
|
||||
declare export function init (value:number):
|
||||
Model
|
||||
|
||||
declare export function update (model:Model, action:Action):
|
||||
Model
|
||||
|
||||
declare export function view (model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -1,18 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as Counter from "./counter"
|
||||
import {start} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
|
||||
const app = start({
|
||||
initial: Counter.create(window.app != null ?
|
||||
window.app.model.value :
|
||||
{value: 0}),
|
||||
update: Counter.update,
|
||||
view: Counter.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
const renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {init, update, view} from "./counter"
|
||||
import {start, beginner} from "reflex"
|
||||
|
||||
export const app = start
|
||||
( beginner
|
||||
( { model:
|
||||
( window.app == null
|
||||
? init(0)
|
||||
: window.app.model.value
|
||||
)
|
||||
, update: update
|
||||
, view: view
|
||||
}
|
||||
)
|
||||
)
|
||||
window.app = app
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {app} from "./main"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
const renderer = new Renderer
|
||||
( { target: document.body
|
||||
}
|
||||
)
|
||||
|
||||
app.view.subscribe
|
||||
( renderer.address
|
||||
)
|
||||
|
||||
app.task.subscribe
|
||||
( Effects.driver(app.address)
|
||||
)
|
|
@ -1,15 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
|
||||
export type Model = {type: "Counter.Model", value:number}
|
||||
export type Increment = {type: "Counter.Increment"}
|
||||
export type Decrement = {type: "Counter.Decrement"}
|
||||
export type Action = Increment|Decrement
|
||||
|
||||
export type asIncrement = () => Increment
|
||||
export type asDecrement = () => Decrement
|
||||
|
||||
export type create = (options:{value:number}) => Model
|
||||
export type update = (model:Model, action:Action) => Model
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/examlpes/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-driver/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-driver' -> 'reflex-virtual-dom-driver/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Counter
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to [demo][]
|
||||
|
||||
|
||||
[demo]:http://localhost:6061/
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
|
@ -16,120 +14,119 @@ import sequencial from 'gulp-sequence';
|
|||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
import manifest from './package.json';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
class Bundler {
|
||||
constructor(options) {
|
||||
this.options = options
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
this.build = this.build.bind(this);
|
||||
this.plugin = []
|
||||
this.transform = []
|
||||
this.cache = {}
|
||||
this.entry = path.join
|
||||
( options.input
|
||||
, options.main
|
||||
)
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
if (options.babel != null) {
|
||||
this.transform.push(babelify)
|
||||
}
|
||||
|
||||
if (options.hotreload != null) {
|
||||
this.plugin.push(hmr)
|
||||
this.transform.push(hotify)
|
||||
}
|
||||
|
||||
this.bundler = browserify
|
||||
( { entries: [this.entry]
|
||||
, debug:
|
||||
( options.sourceMaps == null
|
||||
? false
|
||||
: options.sourceMaps === false
|
||||
? false
|
||||
: true
|
||||
)
|
||||
, cache: this.cache
|
||||
, transform: this.transform
|
||||
, plugin: this.plugin
|
||||
}
|
||||
)
|
||||
|
||||
if (options.watch) {
|
||||
this.watcher =
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build)
|
||||
}
|
||||
}
|
||||
bundle() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
const output =
|
||||
( this.options.watch
|
||||
? this.watcher.bundle()
|
||||
: this.bundler.bundle()
|
||||
)
|
||||
return output
|
||||
}
|
||||
build() {
|
||||
const transforms =
|
||||
[ source(this.options.main)
|
||||
, buffer()
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.init({loadMaps: true})
|
||||
)
|
||||
, ( this.options.compression == null
|
||||
? null
|
||||
: uglify(this.options.compression)
|
||||
)
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.write(this.options.sourceMaps.output)
|
||||
)
|
||||
, gulp.dest(this.options.output)
|
||||
]
|
||||
|
||||
const output =
|
||||
transforms.reduce
|
||||
( (input, transform) =>
|
||||
( transform != null
|
||||
? input.pipe(transform)
|
||||
: input
|
||||
)
|
||||
, this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return output.on('end', () => gutil.log(`Completed bundling: '${this.entry}'`))
|
||||
}
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = config => () => {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
root: path.join(module.filename, config.root),
|
||||
cache: config.cache
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
server.listen(config.port);
|
||||
gutil.log(`Navigate to http://localhost:${config.port}/`)
|
||||
}
|
||||
var bundler = config => () => new Bundler(config).build()
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
Object.keys(manifest.builds).forEach(name => {
|
||||
const config = manifest.builds[name]
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
||||
if (config.server) {
|
||||
gulp.task(`serve ${name}`, server(config.server))
|
||||
gulp.task(`build ${name}`, bundler(config))
|
||||
gulp.task(name, sequencial(`build ${name}`, `serve ${name}`))
|
||||
}
|
||||
else {
|
||||
gulp.task(name, bundler(config))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
<main>
|
||||
</main>
|
||||
<script src="./dist/virtual-dom-driver.js"></script>
|
||||
<scrit src="./dist/reflex-driver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
{
|
||||
"name": "gif-viewer-list",
|
||||
"description": "Reflex random gif viewer list example",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
"start": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-driver": "latest"
|
||||
"reflex": "0.2.0",
|
||||
"reflex-virtual-dom-driver": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"babel-core": "6.7.2",
|
||||
"babel-plugin-remove-comments": "2.0.0",
|
||||
"babel-plugin-syntax-flow": "6.3.13",
|
||||
"babel-plugin-transform-es2015-modules-umd": "6.4.3",
|
||||
"babel-plugin-transform-flow-strip-types": "6.4.0",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babelify": "7.2.0",
|
||||
"browserify": "13.0.0",
|
||||
"browserify-hmr": "0.3.1",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"flow-bin": "0.22.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-sequence": "0.4.5",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"gulp-uglify": "1.5.3",
|
||||
"gulp-util": "3.0.7",
|
||||
"hotify": "0.1.0",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
"builds": {
|
||||
"default": {
|
||||
"//compression": {
|
||||
"mangle": true,
|
||||
"compress": true,
|
||||
"acorn": true
|
||||
},
|
||||
"babel": true,
|
||||
"hotreload": true,
|
||||
"watch": true,
|
||||
"server": {
|
||||
"port": 6061,
|
||||
"cache": 0,
|
||||
"root": "../"
|
||||
},
|
||||
"sourceMaps": {
|
||||
"output": "./"
|
||||
},
|
||||
"main": "./virtual-dom-driver.js",
|
||||
"input": "./src/",
|
||||
"output": "./dist/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
/*:: import * as type from "../type/array-find" */
|
||||
|
||||
export const find/*:type.find*/ = Array.prototype.find != null ?
|
||||
(array, p) => array.find(p) :
|
||||
(array, p) => {
|
||||
let index = 0
|
||||
while (index < array.length) {
|
||||
if (p(array[index])) {
|
||||
return array[index]
|
||||
}
|
||||
index = index + 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/* @flow */
|
||||
|
||||
export const find =
|
||||
( Array.prototype.find != null
|
||||
? /*::<a>*/ (array/*:Array<a>*/, p/*:(a:a) => boolean*/)/*:?a*/ =>
|
||||
array.find(p)
|
||||
: /*::<a>*/ (array/*:Array<a>*/, p/*:(a:a) => boolean*/)/*:?a*/ => {
|
||||
let index = 0
|
||||
while (index < array.length) {
|
||||
if (p(array[index])) {
|
||||
return array[index]
|
||||
}
|
||||
index = index + 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const set = /*::<a>*/
|
||||
( index/*:number*/
|
||||
, element/*:a*/
|
||||
, array/*:Array<a>*/
|
||||
)/*:Array<a>*/ => {
|
||||
if (array[index] === element) {
|
||||
return array
|
||||
}
|
||||
else {
|
||||
const next = array.slice(0)
|
||||
next[index] = element
|
||||
return next
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* @flow */
|
||||
|
||||
declare export function find <a>
|
||||
(array:Array<a>, p:(a:a) => boolean): ?a
|
||||
|
||||
declare export function set <a>
|
||||
( index:number
|
||||
, element:a
|
||||
, array:Array<a>
|
||||
): Array<a>
|
|
@ -1,30 +1,35 @@
|
|||
/* @flow */
|
||||
|
||||
/*:: import * as type from "../type/fetch" */
|
||||
/*::
|
||||
import type {Response} from "./fetch"
|
||||
*/
|
||||
|
||||
export const fetch/*:type.fetch*/ = global.fetch != null ?
|
||||
global.fetch :
|
||||
uri => new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest()
|
||||
request.open("GET", uri, true)
|
||||
request.onload = () => {
|
||||
const status = request.status === 1223 ? 204 : request.status
|
||||
if (status < 100 || status > 599) {
|
||||
reject(Error("Network request failed"))
|
||||
} else {
|
||||
resolve({
|
||||
status,
|
||||
statusText: request.statusText,
|
||||
json() {
|
||||
return new Promise(resolve => {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
})
|
||||
}
|
||||
})
|
||||
export const fetch =
|
||||
( window.fetch != null
|
||||
? window.fetch
|
||||
: (uri/*:string*/)/*:Promise<Response>*/ =>
|
||||
new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest()
|
||||
request.open("GET", uri, true)
|
||||
request.onload = () => {
|
||||
const status = request.status === 1223 ? 204 : request.status
|
||||
if (status < 100 || status > 599) {
|
||||
reject(Error("Network request failed"))
|
||||
} else {
|
||||
resolve({
|
||||
status,
|
||||
statusText: request.statusText,
|
||||
json() {
|
||||
return new Promise(resolve => {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
request.onerror = () => {
|
||||
reject(Error("Network request failed"))
|
||||
}
|
||||
request.send()
|
||||
})
|
||||
request.onerror = () => {
|
||||
reject(Error("Network request failed"))
|
||||
}
|
||||
request.send()
|
||||
})
|
||||
)
|
||||
|
|
|
@ -13,4 +13,6 @@ export type Response = {
|
|||
json: ()=> Promise<JSONValue>
|
||||
}
|
||||
|
||||
export type fetch = (uri:string) => Promise<Response>
|
||||
declare export function fetch
|
||||
(uri:string):
|
||||
Promise<Response>
|
|
@ -1,19 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as RandemGifList from "./random-gif-list"
|
||||
import {start, Effects} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
|
||||
var app = start({
|
||||
initial: window.app != null ?
|
||||
[RandemGifList.create(window.app.model.value)] :
|
||||
RandemGifList.initialize(),
|
||||
step: RandemGifList.step,
|
||||
view: RandemGifList.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
var renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
||||
app.task.subscribe(Effects.service(app.address))
|
|
@ -0,0 +1,22 @@
|
|||
/* @flow */
|
||||
|
||||
import * as RandomGifList from "./random-gif-list"
|
||||
import {start, Effects} from "reflex"
|
||||
|
||||
const restore =
|
||||
_ =>
|
||||
[ window.app.model.value
|
||||
, Effects.none
|
||||
]
|
||||
|
||||
export const app = start
|
||||
( { flags: void(0)
|
||||
, init: ( window.app != null
|
||||
? restore
|
||||
: RandomGifList.init
|
||||
)
|
||||
, update: RandomGifList.update
|
||||
, view: RandomGifList.view
|
||||
}
|
||||
)
|
||||
window.app = app
|
|
@ -1,83 +1,178 @@
|
|||
/* @flow */
|
||||
|
||||
import * as RandomGif from "./random-gif"
|
||||
import {find} from "./array-find"
|
||||
import * as array from "./array"
|
||||
import {html, forward, thunk, Effects} from "reflex"
|
||||
|
||||
|
||||
/*:: import * as type from "../type/random-gif-list" */
|
||||
/*::
|
||||
import type {ID, Action, Model} from "./random-gif-list"
|
||||
import type {Address, DOM} from "reflex"
|
||||
*/
|
||||
|
||||
export const create/*:type.create*/ = ({topic, entries, nextID}) =>
|
||||
({type: "RandomGifList.Model", topic, entries, nextID})
|
||||
const Create =
|
||||
() =>
|
||||
( { type: "Create"
|
||||
}
|
||||
)
|
||||
|
||||
export const initialize/*:type.initialize*/ = () =>
|
||||
[create({topic: "", entries: [], nextID: 0}, Effects.none)]
|
||||
const Topic =
|
||||
(subject:string) =>
|
||||
( { type: "Topic"
|
||||
, source: subject
|
||||
}
|
||||
)
|
||||
|
||||
export const asTopic/*:type.asTopic*/ = topic =>
|
||||
({type: "RandomGifList.Topic", topic})
|
||||
const Modify =
|
||||
(name/*:ID*/) =>
|
||||
(action/*:RandomGif.Action*/)/*:Action*/ =>
|
||||
( { type: "Modify"
|
||||
, source:
|
||||
{ name
|
||||
, action
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const asCreate/*:type.asCreate*/ = () =>
|
||||
({type: "RandomGifList.Create"})
|
||||
const nofx =
|
||||
model =>
|
||||
[ model
|
||||
, Effects.none
|
||||
]
|
||||
|
||||
export const asByID/*:type.asByID*/ = id => act =>
|
||||
({type: "RandomGifList.UpdateByID", id, act})
|
||||
export const init =
|
||||
()/*:[Model, Effects<Action>]*/ =>
|
||||
nofx
|
||||
( { topic: ""
|
||||
, nextID: 0
|
||||
, viewers: []
|
||||
}
|
||||
)
|
||||
|
||||
export const step/*:type.step*/ = (model, action) => {
|
||||
if (action.type === "RandomGifList.Topic") {
|
||||
return [
|
||||
create({
|
||||
topic: action.topic,
|
||||
nextID: model.nextID,
|
||||
entries: model.entries
|
||||
}),
|
||||
Effects.none
|
||||
]
|
||||
}
|
||||
|
||||
if (action.type === "RandomGifList.Create") {
|
||||
const [gif, fx] = RandomGif.initialize(model.topic)
|
||||
return [
|
||||
create({
|
||||
topic: "",
|
||||
nextID: model.nextID + 1,
|
||||
entries: model.entries.concat([{
|
||||
type: "RandomGifList.Entry",
|
||||
id: model.nextID,
|
||||
model: gif
|
||||
}])
|
||||
}),
|
||||
fx.map(asByID(model.nextID))
|
||||
]
|
||||
}
|
||||
|
||||
if (action.type === "RandomGifList.UpdateByID") {
|
||||
const {id} = action
|
||||
const {entries, topic, nextID} = model
|
||||
const entry = find(entries, entry => entry.id === id)
|
||||
const index = entry != null ? entries.indexOf(entry) : -1
|
||||
if (index >= 0 && entry != null && entry.model != null && entry.id != null){
|
||||
const [gif, fx] = RandomGif.step(entry.model, action.act)
|
||||
const entries = model.entries.slice(0)
|
||||
entries[index] = {
|
||||
type: "RandomGifList.Entry",
|
||||
id,
|
||||
model: gif
|
||||
export const update =
|
||||
(model/*:Model*/, action/*:Action*/)/*:[Model, Effects<Action>]*/ =>
|
||||
( action.type === "Create"
|
||||
? createViewer(model)
|
||||
: action.type === "Topic"
|
||||
? updateTopic(model, action.source)
|
||||
: action.type === "Modify"
|
||||
? updateNamed(model, action.source)
|
||||
: nofx(model)
|
||||
)
|
||||
|
||||
const updateTopic =
|
||||
(model, topic) =>
|
||||
nofx
|
||||
( { topic
|
||||
, nextID: model.nextID
|
||||
, viewers: model.viewers
|
||||
}
|
||||
)
|
||||
|
||||
const createViewer =
|
||||
(model) => {
|
||||
const [viewer, fx] = RandomGif.init(model.topic)
|
||||
const next =
|
||||
{ topic: ""
|
||||
, nextID: model.nextID + 1
|
||||
, viewers:
|
||||
[ ...model.viewers
|
||||
, { name: model.nextID
|
||||
, viewer
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
create({topic, nextID, entries}),
|
||||
fx.map(asByID(id))
|
||||
const result =
|
||||
[ next
|
||||
, fx.map(Modify(model.nextID))
|
||||
]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return [model, Effects.none]
|
||||
}
|
||||
const updateNamed =
|
||||
(model, {name, action}) => {
|
||||
const named = array.find
|
||||
( model.viewers
|
||||
, viewer => viewer.name === name
|
||||
)
|
||||
|
||||
if (named != null) {
|
||||
const [viewer, fx] = RandomGif.update
|
||||
( named.viewer
|
||||
, action
|
||||
)
|
||||
|
||||
const viewers = array.set
|
||||
( model.viewers.indexOf(named)
|
||||
, { name: named.name
|
||||
, viewer
|
||||
}
|
||||
, model.viewers
|
||||
)
|
||||
|
||||
const next =
|
||||
[ { topic: model.topic
|
||||
, nextID: model.nextID
|
||||
, viewers
|
||||
}
|
||||
, fx.map(Modify(named.name))
|
||||
]
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
return nofx(model)
|
||||
}
|
||||
|
||||
export const view =
|
||||
(model/*:Model*/, address/*:Address<Action>*/)/*:DOM*/ =>
|
||||
html.main
|
||||
( { key: "random-gif-list"
|
||||
}
|
||||
, [ html.form
|
||||
( { onSubmit: forward(address, decodeSubmit)
|
||||
}
|
||||
, [ html.input
|
||||
( { style: style.input
|
||||
, placeholder: "What kind of gifs do you want?"
|
||||
, value: model.topic
|
||||
, onChange: forward(address, decodeTopicChange)
|
||||
//, onChange: forward(address, event => Topic(event.target.value))
|
||||
// , onKeyUp: event => {
|
||||
// if (event.keyCode === 13) {
|
||||
// address(asCreate())
|
||||
// }
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "random-gifs-list-box"
|
||||
, style: style.container
|
||||
}
|
||||
, model.viewers.map(named => viewNamed(named, address))
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
const decodeSubmit =
|
||||
event =>
|
||||
Create(event.preventDefault())
|
||||
|
||||
const decodeTopicChange =
|
||||
event =>
|
||||
Topic(event.target.value)
|
||||
|
||||
|
||||
const style = {
|
||||
input: {
|
||||
width: "100%",
|
||||
height: "40px",
|
||||
padding: "10px 0",
|
||||
margin: "10px 0",
|
||||
fontSize: "2em",
|
||||
textAlign: "center"
|
||||
},
|
||||
|
@ -87,25 +182,11 @@ const style = {
|
|||
}
|
||||
}
|
||||
|
||||
export const viewEntry/*:type.viewEntry*/ = ({id, model}, address) =>
|
||||
RandomGif.view(model, forward(address, asByID(id)))
|
||||
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "random-gif-list"}, [
|
||||
html.input({
|
||||
style: style.input,
|
||||
placeholder: "What kind of gifs do you want?",
|
||||
value: model.topic,
|
||||
onChange: forward(address, event => asTopic(event.target.value)),
|
||||
onKeyUp: event => {
|
||||
if (event.keyCode === 13) {
|
||||
address(asCreate())
|
||||
}
|
||||
}
|
||||
}),
|
||||
html.div({key: "random-gifs-list-box", style: style.container},
|
||||
model.entries.map(entry => thunk(String(entry.id),
|
||||
viewEntry,
|
||||
entry,
|
||||
address)))
|
||||
])
|
||||
const viewNamed =
|
||||
(model, address) =>
|
||||
thunk
|
||||
( String(model.name)
|
||||
, RandomGif.view
|
||||
, model.viewer
|
||||
, forward(address, Modify(model.name))
|
||||
)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/* @flow */
|
||||
|
||||
import {Effects} from "reflex"
|
||||
import type {Address, DOM} from "reflex"
|
||||
import * as RandomGif from "./random-gif"
|
||||
|
||||
export type ID = number
|
||||
|
||||
type NamedViewer =
|
||||
{ name: ID
|
||||
, viewer: RandomGif.Model
|
||||
}
|
||||
|
||||
export type Model =
|
||||
{ topic: string
|
||||
, nextID: ID
|
||||
, viewers: Array<NamedViewer>
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "Create" }
|
||||
| { type: "Topic"
|
||||
, source: string
|
||||
}
|
||||
| { type: "Modify"
|
||||
, source:
|
||||
{ name: ID
|
||||
, action: RandomGif.Action
|
||||
}
|
||||
}
|
||||
|
||||
declare export function init ():
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -3,76 +3,139 @@
|
|||
import {html, forward, Effects, Task} from "reflex"
|
||||
import {fetch} from "./fetch"
|
||||
|
||||
/*:: import * as type from "../type/random-gif" */
|
||||
|
||||
export const create/*:type.create*/ = ({topic, uri}) =>
|
||||
({type: "RandomGif.Model", topic, uri})
|
||||
/*::
|
||||
import type {URI, Model, Action} from "./random-gif"
|
||||
import type {Address, DOM, Never} from "reflex"
|
||||
*/
|
||||
|
||||
export const initialize/*:type.initialize*/ = topic =>
|
||||
[create({topic, uri: "assets/waiting.gif"}), getRandomGif(topic)]
|
||||
const RequestMore =
|
||||
() =>
|
||||
( { type: "RequestMore"
|
||||
}
|
||||
)
|
||||
|
||||
const ReceiveNewGif =
|
||||
(uri) =>
|
||||
( { type: "ReceiveNewGif"
|
||||
, source: uri
|
||||
}
|
||||
)
|
||||
|
||||
export const asRequestMore/*:type.asRequestMore*/ = () =>
|
||||
({type: "RandomGif.RequestMore"})
|
||||
export const init =
|
||||
(topic/*:string*/)/*:[Model, Effects<Action>]*/ =>
|
||||
requestMore
|
||||
( { topic
|
||||
, uri: "assets/waiting.gif"
|
||||
}
|
||||
)
|
||||
|
||||
export const asReceiveNewGif/*:type.asReceiveNewGif*/ = uri =>
|
||||
({type: "RandomGif.ReceiveNewGif", uri})
|
||||
const nofx =
|
||||
(model/*:Model*/)/*:[Model, Effects<Action>]*/ =>
|
||||
[ model
|
||||
, Effects.none
|
||||
]
|
||||
|
||||
|
||||
export const step/*:type.step*/ = (model, action) =>
|
||||
action.type === "RandomGif.RequestMore" ?
|
||||
[model, getRandomGif(model.topic)] :
|
||||
action.type === "RandomGif.ReceiveNewGif" ?
|
||||
[
|
||||
create({
|
||||
topic: model.topic,
|
||||
uri: action.uri != null ? action.uri : model.uri
|
||||
}),
|
||||
Effects.none
|
||||
] :
|
||||
[model, Effects.none]
|
||||
|
||||
const makeRandomURI = topic =>
|
||||
const makeRandomURI =
|
||||
topic =>
|
||||
`http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}`
|
||||
|
||||
const decodeResponseBody = body =>
|
||||
(body != null && body.data != null && body.data.image_url != null) ?
|
||||
String(body.data.image_url) :
|
||||
null
|
||||
|
||||
const readResponseAsJSON = response => response.json()
|
||||
const decodeResponseBody =
|
||||
body =>
|
||||
( body == null
|
||||
? null
|
||||
: ( body.data == null
|
||||
? null
|
||||
: ( body.data.image_url == null
|
||||
? null
|
||||
: String(body.data.image_url)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const getRandomGif/*:type.getRandomGif*/ = topic =>
|
||||
Effects.task(Task.future(() => fetch(makeRandomURI(topic))
|
||||
.then(readResponseAsJSON)
|
||||
.then(decodeResponseBody)
|
||||
.then(asReceiveNewGif)))
|
||||
|
||||
const style = {
|
||||
viewer: {
|
||||
width: "200px"
|
||||
},
|
||||
header: {
|
||||
width: "200px",
|
||||
textAlign: "center"
|
||||
},
|
||||
image(uri) {
|
||||
return {
|
||||
display: "inline-block",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
backgroundPosition: "center center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage: `url("${uri}")`
|
||||
const getRandomGif =
|
||||
(topic) =>
|
||||
Task.future
|
||||
( () =>
|
||||
fetch(makeRandomURI(topic))
|
||||
.then(response => response.json())
|
||||
.then(decodeResponseBody)
|
||||
)
|
||||
|
||||
|
||||
|
||||
const requestMore =
|
||||
model =>
|
||||
[ model
|
||||
, Effects.task(getRandomGif(model.topic))
|
||||
.map(ReceiveNewGif)
|
||||
]
|
||||
|
||||
const receivedNewGif =
|
||||
(model, uri) =>
|
||||
nofx
|
||||
( { topic: model.topic
|
||||
, uri:
|
||||
( uri == null
|
||||
? model.uri
|
||||
: uri
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "gif-viewer", style: style.viewer}, [
|
||||
html.h2({key: "header", style: style.header}, [model.topic]),
|
||||
html.div({key: "image", style: style.image(model.uri)}),
|
||||
html.button({key: "button", onClick: forward(address, asRequestMore)}, [
|
||||
"More please!"
|
||||
])
|
||||
])
|
||||
export const update =
|
||||
( model/*:Model*/, action/*:Action*/)/*:[Model, Effects<Action>]*/ =>
|
||||
( action.type === "ReceiveNewGif"
|
||||
? receivedNewGif(model, action.source)
|
||||
: action.type === "RequestMore"
|
||||
? requestMore(model)
|
||||
: nofx(model)
|
||||
)
|
||||
|
||||
|
||||
const style =
|
||||
{ viewer:
|
||||
{ width: "200px"
|
||||
}
|
||||
, header:
|
||||
{ width: "200px"
|
||||
, textAlign: "center"
|
||||
}
|
||||
, image: (uri) =>
|
||||
( { display: "inline-block"
|
||||
, width: "200px"
|
||||
, height: "200px"
|
||||
, backgroundPosition: "center center"
|
||||
, backgroundSize: "cover"
|
||||
, backgroundImage: `url("${uri}")`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const view =
|
||||
(model/*:Model*/, address/*:Address<Action>*/)/*:DOM*/ =>
|
||||
html.main
|
||||
( { key: "gif-viewer"
|
||||
, style: style.viewer
|
||||
}
|
||||
, [ html.h2
|
||||
( { key: "header"
|
||||
, style: style.header
|
||||
}
|
||||
, [ model.topic ]
|
||||
)
|
||||
, html.div
|
||||
( { key: "image"
|
||||
, style: style.image(model.uri)
|
||||
}
|
||||
)
|
||||
, html.button
|
||||
( { key: "button"
|
||||
, onClick: forward(address, RequestMore)
|
||||
}
|
||||
, [ "More please!" ]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/* @flow */
|
||||
|
||||
import type {DOM, Address} from "reflex"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
export type URI = string
|
||||
|
||||
export type Model =
|
||||
{ topic: string
|
||||
, uri: URI
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "RequestMore" }
|
||||
| { type: "ReceiveNewGif"
|
||||
, source: ?URI
|
||||
}
|
||||
|
||||
declare export function init
|
||||
(topic:string):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {app} from "./main"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
const renderer = new Renderer
|
||||
( { target: document.body
|
||||
}
|
||||
)
|
||||
|
||||
app.view.subscribe
|
||||
( renderer.address
|
||||
)
|
||||
|
||||
app.task.subscribe
|
||||
( Effects.driver(app.address)
|
||||
)
|
|
@ -1,3 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
export type find <a> = (array:Array<a>, p:(a:a) => boolean) => ?a
|
|
@ -1,54 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Effects} from "reflex/type/effects"
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
import * as RandomGif from "./random-gif"
|
||||
|
||||
export type ID = number
|
||||
|
||||
export type Entry = {
|
||||
type: "RandomGifList.Entry",
|
||||
id: ID,
|
||||
model: RandomGif.Model
|
||||
}
|
||||
|
||||
export type State = {
|
||||
topic: string,
|
||||
nextID: ID,
|
||||
entries: Array<Entry>
|
||||
}
|
||||
|
||||
export type Model = {
|
||||
type: "RandomGifList.Model",
|
||||
topic: string,
|
||||
nextID: ID,
|
||||
entries: Array<Entry>
|
||||
}
|
||||
|
||||
|
||||
export type Topic = {type: "RandomGifList.Topic", topic: string}
|
||||
export type Create = {type: "RandomGifList.Create"}
|
||||
export type UpdateByID = {
|
||||
type: "RandomGifList.UpdateByID",
|
||||
id: ID,
|
||||
act: RandomGif.Action
|
||||
}
|
||||
|
||||
export type Action
|
||||
= Topic
|
||||
| Create
|
||||
| UpdateByID
|
||||
|
||||
|
||||
|
||||
export type asTopic = (topic:string) => Topic
|
||||
export type asCreate = () => Create
|
||||
export type asByID = (id:ID) => (act:RandomGif.Action) => UpdateByID
|
||||
|
||||
export type create = (options:State) => Model
|
||||
export type initialize = () => [Model, Effects<Action>]
|
||||
|
||||
export type step = (model:Model, action:Action) => [Model, Effects<Action>]
|
||||
|
||||
export type viewEntry = (entry:Entry, address:Address<Action>) => VirtualNode
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -1,32 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Effects} from "reflex/type/effects"
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
|
||||
export type State = {
|
||||
topic: string,
|
||||
uri: string
|
||||
}
|
||||
|
||||
export type Model
|
||||
= {type: "RandomGif.Model"}
|
||||
& State
|
||||
|
||||
export type RequestMore = {type: "RandomGif.RequestMore"}
|
||||
export type ReceiveNewGif = {type: "RandomGif.ReceiveNewGif", uri: ?string}
|
||||
export type Action
|
||||
= RequestMore
|
||||
| ReceiveNewGif
|
||||
|
||||
export type asRequestMore = () => RequestMore
|
||||
export type asReceiveNewGif = (uri:?string) => ReceiveNewGif
|
||||
|
||||
export type create = (options:State) => Model
|
||||
|
||||
|
||||
export type initialize = (topic:string) => [Model, Effects<Action>]
|
||||
export type step = (model:Model, action:Action) => [Model, Effects<Action>]
|
||||
|
||||
export type getRandomGif = (topic:string) => Effects<Action>
|
||||
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"sourceMaps": "inline",
|
||||
"comments": false,
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-flow",
|
||||
"transform-flow-strip-types",
|
||||
"remove-comments"
|
||||
]
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/tap/.*
|
||||
.*/node_modules/reflex-virtual-dom-driver/examlpes/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-driver/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-driver' -> 'reflex-virtual-dom-driver/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
||||
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIssue
|
||||
suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Counter
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Navigate to [demo][]
|
||||
|
||||
|
||||
[demo]:http://localhost:6061/
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
|
@ -16,120 +14,119 @@ import sequencial from 'gulp-sequence';
|
|||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
import manifest from './package.json';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
class Bundler {
|
||||
constructor(options) {
|
||||
this.options = options
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
this.build = this.build.bind(this);
|
||||
this.plugin = []
|
||||
this.transform = []
|
||||
this.cache = {}
|
||||
this.entry = path.join
|
||||
( options.input
|
||||
, options.main
|
||||
)
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
if (options.babel != null) {
|
||||
this.transform.push(babelify)
|
||||
}
|
||||
|
||||
if (options.hotreload != null) {
|
||||
this.plugin.push(hmr)
|
||||
this.transform.push(hotify)
|
||||
}
|
||||
|
||||
this.bundler = browserify
|
||||
( { entries: [this.entry]
|
||||
, debug:
|
||||
( options.sourceMaps == null
|
||||
? false
|
||||
: options.sourceMaps === false
|
||||
? false
|
||||
: true
|
||||
)
|
||||
, cache: this.cache
|
||||
, transform: this.transform
|
||||
, plugin: this.plugin
|
||||
}
|
||||
)
|
||||
|
||||
if (options.watch) {
|
||||
this.watcher =
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build)
|
||||
}
|
||||
}
|
||||
bundle() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
const output =
|
||||
( this.options.watch
|
||||
? this.watcher.bundle()
|
||||
: this.bundler.bundle()
|
||||
)
|
||||
return output
|
||||
}
|
||||
build() {
|
||||
const transforms =
|
||||
[ source(this.options.main)
|
||||
, buffer()
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.init({loadMaps: true})
|
||||
)
|
||||
, ( this.options.compression == null
|
||||
? null
|
||||
: uglify(this.options.compression)
|
||||
)
|
||||
, ( this.options.sourceMaps == null
|
||||
? null
|
||||
: sourcemaps.write(this.options.sourceMaps.output)
|
||||
)
|
||||
, gulp.dest(this.options.output)
|
||||
]
|
||||
|
||||
const output =
|
||||
transforms.reduce
|
||||
( (input, transform) =>
|
||||
( transform != null
|
||||
? input.pipe(transform)
|
||||
: input
|
||||
)
|
||||
, this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return output.on('end', () => gutil.log(`Completed bundling: '${this.entry}'`))
|
||||
}
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = config => () => {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
root: path.join(module.filename, config.root),
|
||||
cache: config.cache
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
server.listen(config.port);
|
||||
gutil.log(`Navigate to http://localhost:${config.port}/`)
|
||||
}
|
||||
var bundler = config => () => new Bundler(config).build()
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
Object.keys(manifest.builds).forEach(name => {
|
||||
const config = manifest.builds[name]
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
||||
if (config.server) {
|
||||
gulp.task(`serve ${name}`, server(config.server))
|
||||
gulp.task(`build ${name}`, bundler(config))
|
||||
gulp.task(name, sequencial(`build ${name}`, `serve ${name}`))
|
||||
}
|
||||
else {
|
||||
gulp.task(name, bundler(config))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
<main>
|
||||
</main>
|
||||
<script src="./dist/virtual-dom-driver.js"></script>
|
||||
<scrit src="./dist/reflex-driver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
{
|
||||
"name": "gif-viewer-pair",
|
||||
"description": "Reflex random gif pair example",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
"start": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-driver": "latest"
|
||||
"reflex": "0.2.0",
|
||||
"reflex-virtual-dom-driver": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"babel-core": "6.7.2",
|
||||
"babel-plugin-remove-comments": "2.0.0",
|
||||
"babel-plugin-syntax-flow": "6.3.13",
|
||||
"babel-plugin-transform-es2015-modules-umd": "6.4.3",
|
||||
"babel-plugin-transform-flow-strip-types": "6.4.0",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babelify": "7.2.0",
|
||||
"browserify": "13.0.0",
|
||||
"browserify-hmr": "0.3.1",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"flow-bin": "0.22.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-sequence": "0.4.5",
|
||||
"gulp-sourcemaps": "1.6.0",
|
||||
"gulp-uglify": "1.5.3",
|
||||
"gulp-util": "3.0.7",
|
||||
"hotify": "0.1.0",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
"builds": {
|
||||
"default": {
|
||||
"//compression": {
|
||||
"mangle": true,
|
||||
"compress": true,
|
||||
"acorn": true
|
||||
},
|
||||
"babel": true,
|
||||
"hotreload": true,
|
||||
"watch": true,
|
||||
"server": {
|
||||
"port": 6061,
|
||||
"cache": 0,
|
||||
"root": "../"
|
||||
},
|
||||
"sourceMaps": {
|
||||
"output": "./"
|
||||
},
|
||||
"main": "./virtual-dom-driver.js",
|
||||
"input": "./src/",
|
||||
"output": "./dist/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
/* @flow */
|
||||
|
||||
/*:: import * as type from "../type/fetch" */
|
||||
/*::
|
||||
import type {Response} from "./fetch"
|
||||
*/
|
||||
|
||||
export const fetch/*:type.fetch*/ = global.fetch != null ?
|
||||
global.fetch :
|
||||
uri => new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest()
|
||||
request.open("GET", uri, true)
|
||||
request.onload = () => {
|
||||
const status = request.status === 1223 ? 204 : request.status
|
||||
if (status < 100 || status > 599) {
|
||||
reject(Error("Network request failed"))
|
||||
} else {
|
||||
resolve({
|
||||
status,
|
||||
statusText: request.statusText,
|
||||
json() {
|
||||
return new Promise(resolve => {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
})
|
||||
}
|
||||
})
|
||||
export const fetch =
|
||||
( window.fetch != null
|
||||
? window.fetch
|
||||
: (uri/*:string*/)/*:Promise<Response>*/ =>
|
||||
new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest()
|
||||
request.open("GET", uri, true)
|
||||
request.onload = () => {
|
||||
const status = request.status === 1223 ? 204 : request.status
|
||||
if (status < 100 || status > 599) {
|
||||
reject(Error("Network request failed"))
|
||||
} else {
|
||||
resolve({
|
||||
status,
|
||||
statusText: request.statusText,
|
||||
json() {
|
||||
return new Promise(resolve => {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
request.onerror = () => {
|
||||
reject(Error("Network request failed"))
|
||||
}
|
||||
request.send()
|
||||
})
|
||||
request.onerror = () => {
|
||||
reject(Error("Network request failed"))
|
||||
}
|
||||
request.send()
|
||||
})
|
||||
)
|
||||
|
|
|
@ -13,4 +13,6 @@ export type Response = {
|
|||
json: ()=> Promise<JSONValue>
|
||||
}
|
||||
|
||||
export type fetch = (uri:string) => Promise<Response>
|
||||
declare export function fetch
|
||||
(uri:string):
|
||||
Promise<Response>
|
|
@ -1,19 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import * as RandomGifPair from "./random-gif-pair"
|
||||
import {start, Effects} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
|
||||
var app = start({
|
||||
initial: window.app != null ?
|
||||
[RandomGifPair.create(window.app.model.value)] :
|
||||
RandomGifPair.initialize("funny cats", "funny hamsters"),
|
||||
step: RandomGifPair.step,
|
||||
view: RandomGifPair.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
var renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
||||
app.task.subscribe(Effects.service(app.address))
|
|
@ -0,0 +1,25 @@
|
|||
/* @flow */
|
||||
|
||||
import * as RandomGifPair from "./random-gif-pair"
|
||||
import {start, Effects} from "reflex"
|
||||
|
||||
const restore =
|
||||
_ =>
|
||||
[ window.app.model.value
|
||||
, Effects.none
|
||||
]
|
||||
|
||||
export const app = start
|
||||
( { flags:
|
||||
[ "funny cats"
|
||||
, "funny dogs"
|
||||
]
|
||||
, init: ( window.app != null
|
||||
? restore
|
||||
: RandomGifPair.init
|
||||
)
|
||||
, update: RandomGifPair.update
|
||||
, view: RandomGifPair.view
|
||||
}
|
||||
)
|
||||
window.app = app
|
|
@ -1,57 +1,104 @@
|
|||
/* @flow */
|
||||
|
||||
import * as RandomGif from "./random-gif"
|
||||
import {html, forward, Task, Effects} from "reflex"
|
||||
import * as RandomGif from "./random-gif"
|
||||
|
||||
/*:: import * as type from "../type/random-gif-pair" */
|
||||
|
||||
export const create/*:type.create*/ = ({left, right}) => ({
|
||||
type: "RandomGifPair.Model",
|
||||
left: RandomGif.create(left),
|
||||
right: RandomGif.create(right)
|
||||
})
|
||||
/*::
|
||||
import type {DOM, Address} from "reflex"
|
||||
import type {Model, Action} from "./random-gif-pair"
|
||||
*/
|
||||
|
||||
export const initialize/*:type.initialize*/ = (leftTopic, rightTopic) => {
|
||||
const [left, leftFx] = RandomGif.initialize(leftTopic)
|
||||
const [right, rightFx] = RandomGif.initialize(rightTopic)
|
||||
return [
|
||||
create({left, right}),
|
||||
Effects.batch([
|
||||
leftFx.map(asLeft),
|
||||
rightFx.map(asRight)
|
||||
])
|
||||
const Left =
|
||||
action =>
|
||||
( { type: "Left"
|
||||
, source: action
|
||||
}
|
||||
)
|
||||
|
||||
const Right =
|
||||
action =>
|
||||
( { type: "Right"
|
||||
, source: action
|
||||
}
|
||||
)
|
||||
|
||||
export const init =
|
||||
([leftTopic, rightTopic]/*:[string, string]*/)/*:[Model, Effects<Action>]*/ => {
|
||||
const [left, leftFx] = RandomGif.init(leftTopic)
|
||||
const [right, rightFx] = RandomGif.init(rightTopic)
|
||||
const model = { left, right }
|
||||
const fx = Effects.batch
|
||||
( [ leftFx.map(Left)
|
||||
, rightFx.map(Right)
|
||||
]
|
||||
)
|
||||
|
||||
return [model, fx]
|
||||
}
|
||||
|
||||
const nofx =
|
||||
model =>
|
||||
[ model
|
||||
, Effects.none
|
||||
]
|
||||
}
|
||||
|
||||
export const asLeft/*:type.asLeft*/ = act =>
|
||||
({type: "RandomGifPair.Left", act})
|
||||
|
||||
export const asRight/*:type.asRight*/ = act =>
|
||||
({type: "RandomGifPair.Right", act})
|
||||
|
||||
|
||||
export const step/*:type.step*/ = (model, action) => {
|
||||
if (action.type === "RandomGifPair.Left") {
|
||||
const [left, fx] = RandomGif.step(model.left, action.act)
|
||||
return [create({left, right: model.right}), fx.map(asLeft)]
|
||||
const updateLeft =
|
||||
(model, action) => {
|
||||
const [left, fx] = RandomGif.update(model.left, action)
|
||||
const next =
|
||||
[ { left
|
||||
, right: model.right
|
||||
}
|
||||
, fx.map(Left)
|
||||
]
|
||||
return next
|
||||
}
|
||||
|
||||
if (action.type === "RandomGifPair.Right") {
|
||||
const [right, fx] = RandomGif.step(model.right, action.act)
|
||||
return [create({left:model.left, right}), fx.map(asRight)]
|
||||
const updateRight =
|
||||
(model, action) => {
|
||||
const [right, fx] = RandomGif.update(model.right, action)
|
||||
const next =
|
||||
[ { left: model.left
|
||||
, right
|
||||
}
|
||||
, fx.map(Right)
|
||||
]
|
||||
return next
|
||||
}
|
||||
|
||||
return [model, Effects.none]
|
||||
}
|
||||
export const update =
|
||||
(model/*:Model*/, action/*:Action*/)/*:[Model, Effects<Action>]*/ =>
|
||||
( action.type === "Left"
|
||||
? updateLeft(model, action.source)
|
||||
: action.type === "Right"
|
||||
? updateRight(model, action.source)
|
||||
: nofx(model)
|
||||
)
|
||||
|
||||
export const view/*:type.view*/ = (model, address) => {
|
||||
return html.div({key: "random-gif-pair",
|
||||
style: {display: "flex"}}, [
|
||||
html.div({key: "left"}, [
|
||||
RandomGif.view(model.left, forward(address, asLeft))
|
||||
]),
|
||||
html.div({key: "right"}, [
|
||||
RandomGif.view(model.right, forward(address, asRight))
|
||||
])
|
||||
]);
|
||||
};
|
||||
export const view =
|
||||
(model/*:Model*/, address/*:Address<Action>*/)/*:DOM*/ =>
|
||||
html.div
|
||||
( { key: "random-gif-pair"
|
||||
, style: {display: "flex"}
|
||||
}
|
||||
, [ html.div
|
||||
( { key: "left"
|
||||
}
|
||||
, [ RandomGif.view
|
||||
( model.left
|
||||
, forward(address, Left)
|
||||
)
|
||||
]
|
||||
)
|
||||
, html.div
|
||||
( { key: "right"
|
||||
}
|
||||
, [ RandomGif.view
|
||||
( model.right
|
||||
, forward(address, Right)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/* @flow */
|
||||
|
||||
import {Effects} from "reflex"
|
||||
import type {Address, DOM} from "reflex"
|
||||
import * as RandomGif from "./random-gif"
|
||||
|
||||
export type Model =
|
||||
{ left: RandomGif.Model
|
||||
, right: RandomGif.Model
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "Left"
|
||||
, source: RandomGif.Action
|
||||
}
|
||||
| { type: "Right"
|
||||
, source: RandomGif.Action
|
||||
}
|
||||
|
||||
declare export function init
|
||||
(flags:[string, string]):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -3,76 +3,139 @@
|
|||
import {html, forward, Effects, Task} from "reflex"
|
||||
import {fetch} from "./fetch"
|
||||
|
||||
/*:: import * as type from "../type/random-gif" */
|
||||
|
||||
export const create/*:type.create*/ = ({topic, uri}) =>
|
||||
({type: "RandomGif.Model", topic, uri})
|
||||
/*::
|
||||
import type {URI, Model, Action} from "./random-gif"
|
||||
import type {Address, DOM, Never} from "reflex"
|
||||
*/
|
||||
|
||||
export const initialize/*:type.initialize*/ = topic =>
|
||||
[create({topic, uri: "assets/waiting.gif"}), getRandomGif(topic)]
|
||||
const RequestMore =
|
||||
() =>
|
||||
( { type: "RequestMore"
|
||||
}
|
||||
)
|
||||
|
||||
const ReceiveNewGif =
|
||||
(uri) =>
|
||||
( { type: "ReceiveNewGif"
|
||||
, source: uri
|
||||
}
|
||||
)
|
||||
|
||||
export const asRequestMore/*:type.asRequestMore*/ = () =>
|
||||
({type: "RandomGif.RequestMore"})
|
||||
export const init =
|
||||
(topic/*:string*/)/*:[Model, Effects<Action>]*/ =>
|
||||
requestMore
|
||||
( { topic
|
||||
, uri: "assets/waiting.gif"
|
||||
}
|
||||
)
|
||||
|
||||
export const asReceiveNewGif/*:type.asReceiveNewGif*/ = uri =>
|
||||
({type: "RandomGif.ReceiveNewGif", uri})
|
||||
const nofx =
|
||||
(model/*:Model*/)/*:[Model, Effects<Action>]*/ =>
|
||||
[ model
|
||||
, Effects.none
|
||||
]
|
||||
|
||||
|
||||
export const step/*:type.step*/ = (model, action) =>
|
||||
action.type === "RandomGif.RequestMore" ?
|
||||
[model, getRandomGif(model.topic)] :
|
||||
action.type === "RandomGif.ReceiveNewGif" ?
|
||||
[
|
||||
create({
|
||||
topic: model.topic,
|
||||
uri: action.uri != null ? action.uri : model.uri
|
||||
}),
|
||||
Effects.none
|
||||
] :
|
||||
[model, Effects.none]
|
||||
|
||||
const makeRandomURI = topic =>
|
||||
const makeRandomURI =
|
||||
topic =>
|
||||
`http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}`
|
||||
|
||||
const decodeResponseBody = body =>
|
||||
(body != null && body.data != null && body.data.image_url != null) ?
|
||||
String(body.data.image_url) :
|
||||
null
|
||||
|
||||
const readResponseAsJSON = response => response.json()
|
||||
const decodeResponseBody =
|
||||
body =>
|
||||
( body == null
|
||||
? null
|
||||
: ( body.data == null
|
||||
? null
|
||||
: ( body.data.image_url == null
|
||||
? null
|
||||
: String(body.data.image_url)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const getRandomGif/*:type.getRandomGif*/ = topic =>
|
||||
Effects.task(Task.future(() => fetch(makeRandomURI(topic))
|
||||
.then(readResponseAsJSON)
|
||||
.then(decodeResponseBody)
|
||||
.then(asReceiveNewGif)))
|
||||
|
||||
const style = {
|
||||
viewer: {
|
||||
width: "200px"
|
||||
},
|
||||
header: {
|
||||
width: "200px",
|
||||
textAlign: "center"
|
||||
},
|
||||
image(uri) {
|
||||
return {
|
||||
display: "inline-block",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
backgroundPosition: "center center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage: `url("${uri}")`
|
||||
const getRandomGif =
|
||||
(topic) =>
|
||||
Task.future
|
||||
( () =>
|
||||
fetch(makeRandomURI(topic))
|
||||
.then(response => response.json())
|
||||
.then(decodeResponseBody)
|
||||
)
|
||||
|
||||
|
||||
|
||||
const requestMore =
|
||||
model =>
|
||||
[ model
|
||||
, Effects.task(getRandomGif(model.topic))
|
||||
.map(ReceiveNewGif)
|
||||
]
|
||||
|
||||
const receivedNewGif =
|
||||
(model, uri) =>
|
||||
nofx
|
||||
( { topic: model.topic
|
||||
, uri:
|
||||
( uri == null
|
||||
? model.uri
|
||||
: uri
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const view/*:type.view*/ = (model, address) =>
|
||||
html.div({key: "gif-viewer", style: style.viewer}, [
|
||||
html.h2({key: "header", style: style.header}, [model.topic]),
|
||||
html.div({key: "image", style: style.image(model.uri)}),
|
||||
html.button({key: "button", onClick: forward(address, asRequestMore)}, [
|
||||
"More please!"
|
||||
])
|
||||
])
|
||||
export const update =
|
||||
( model/*:Model*/, action/*:Action*/)/*:[Model, Effects<Action>]*/ =>
|
||||
( action.type === "ReceiveNewGif"
|
||||
? receivedNewGif(model, action.source)
|
||||
: action.type === "RequestMore"
|
||||
? requestMore(model)
|
||||
: nofx(model)
|
||||
)
|
||||
|
||||
|
||||
const style =
|
||||
{ viewer:
|
||||
{ width: "200px"
|
||||
}
|
||||
, header:
|
||||
{ width: "200px"
|
||||
, textAlign: "center"
|
||||
}
|
||||
, image: (uri) =>
|
||||
( { display: "inline-block"
|
||||
, width: "200px"
|
||||
, height: "200px"
|
||||
, backgroundPosition: "center center"
|
||||
, backgroundSize: "cover"
|
||||
, backgroundImage: `url("${uri}")`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const view =
|
||||
(model/*:Model*/, address/*:Address<Action>*/)/*:DOM*/ =>
|
||||
html.main
|
||||
( { key: "gif-viewer"
|
||||
, style: style.viewer
|
||||
}
|
||||
, [ html.h2
|
||||
( { key: "header"
|
||||
, style: style.header
|
||||
}
|
||||
, [ model.topic ]
|
||||
)
|
||||
, html.div
|
||||
( { key: "image"
|
||||
, style: style.image(model.uri)
|
||||
}
|
||||
)
|
||||
, html.button
|
||||
( { key: "button"
|
||||
, onClick: forward(address, RequestMore)
|
||||
}
|
||||
, [ "More please!" ]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/* @flow */
|
||||
|
||||
import type {DOM, Address} from "reflex"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
export type URI = string
|
||||
|
||||
export type Model =
|
||||
{ topic: string
|
||||
, uri: URI
|
||||
}
|
||||
|
||||
export type Action
|
||||
= { type: "RequestMore" }
|
||||
| { type: "ReceiveNewGif"
|
||||
, source: ?URI
|
||||
}
|
||||
|
||||
declare export function init
|
||||
(topic:string):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function update
|
||||
(model:Model, action:Action):
|
||||
[Model, Effects<Action>]
|
||||
|
||||
declare export function view
|
||||
(model:Model, address:Address<Action>):
|
||||
DOM
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import {app} from "./main"
|
||||
import {Renderer} from "reflex-virtual-dom-driver"
|
||||
import {Effects} from "reflex"
|
||||
|
||||
const renderer = new Renderer
|
||||
( { target: document.body
|
||||
}
|
||||
)
|
||||
|
||||
app.view.subscribe
|
||||
( renderer.address
|
||||
)
|
||||
|
||||
app.task.subscribe
|
||||
( Effects.driver(app.address)
|
||||
)
|
|
@ -1,36 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Effects} from "reflex/type/effects"
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
import * as RandomGif from "./random-gif"
|
||||
|
||||
export type State = {
|
||||
left: RandomGif.State,
|
||||
right: RandomGif.State
|
||||
}
|
||||
|
||||
export type Model
|
||||
= {type: "RandomGifPair.Model"}
|
||||
& {left: RandomGif.Model, right:RandomGif.Model}
|
||||
|
||||
export type Left = {
|
||||
type: "RandomGifPair.Left",
|
||||
act: RandomGif.Action
|
||||
}
|
||||
export type Right = {
|
||||
type: "RandomGifPair.Right",
|
||||
act: any // Workaround for facebook/flow#953
|
||||
// act: RandomGif.Action
|
||||
}
|
||||
export type Action = Left | Right
|
||||
|
||||
export type asLeft = (action:RandomGif.Action) => Left
|
||||
export type asRight = (action:RandomGif.Action) => Right
|
||||
|
||||
export type create = (options:State) => Model
|
||||
export type initialize = (leftTopic:string, rightTopic:string) =>
|
||||
[Model, Effects<Action>]
|
||||
|
||||
export type step = (model:Model, action:Action) => [Model, Effects<Action>]
|
||||
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
|
@ -1,32 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
import type {Effects} from "reflex/type/effects"
|
||||
import type {Address, VirtualNode} from "reflex/type"
|
||||
|
||||
export type State = {
|
||||
topic: string,
|
||||
uri: string
|
||||
}
|
||||
|
||||
export type Model
|
||||
= {type: "RandomGif.Model"}
|
||||
& State
|
||||
|
||||
export type RequestMore = {type: "RandomGif.RequestMore"}
|
||||
export type ReceiveNewGif = {type: "RandomGif.ReceiveNewGif", uri: ?string}
|
||||
export type Action
|
||||
= RequestMore
|
||||
| ReceiveNewGif
|
||||
|
||||
export type asRequestMore = () => RequestMore
|
||||
export type asReceiveNewGif = (uri:?string) => ReceiveNewGif
|
||||
|
||||
export type create = (options:State) => Model
|
||||
|
||||
|
||||
export type initialize = (topic:string) => [Model, Effects<Action>]
|
||||
export type step = (model:Model, action:Action) => [Model, Effects<Action>]
|
||||
|
||||
export type getRandomGif = (topic:string) => Effects<Action>
|
||||
|
||||
export type view = (model:Model, address:Address<Action>) => VirtualNode
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче