├── .gitignore ├── demos ├── index.html ├── time-slicing │ ├── index.html │ ├── README.md │ ├── index.js │ ├── index.css │ ├── Clock.js │ └── Charts.js ├── custom.css ├── components │ ├── index.js │ ├── timer.js │ ├── sourceswitching.js │ ├── blink.js │ ├── counter.js │ ├── crappybird.js │ └── button.css ├── eleventy.css └── index.js ├── .babelrc ├── reactive-react ├── index.js ├── updateProperties.js ├── element.js ├── component.js ├── swyxjs.js ├── scheduler.js └── reconciler.js ├── prototypeAPI.js ├── netlify.toml ├── rollup.config.js ├── package.json ├── README.md └── dependencygraph.svg /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn.lock 4 | .cache -------------------------------------------------------------------------------- /demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demos/time-slicing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demos/time-slicing/README.md: -------------------------------------------------------------------------------- 1 | note that this isnt actually a demo of time slicing - its just meant to show the performance issues. 2 | 3 | this was originally written as a separate demo from all the rest and that may change. -------------------------------------------------------------------------------- /demos/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: rgb(2,0,36); 3 | background: linear-gradient(45deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 23%, rgba(161,0,0,1) 100%); 4 | } 5 | html { 6 | overflow: scroll; 7 | } 8 | .container { 9 | width: 700px 10 | } 11 | 12 | .titleLink { 13 | display: flex 14 | } 15 | 16 | .titleLink a { 17 | margin-left: 10px; 18 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-rest-spread", 4 | "transform-class-properties", 5 | [ 6 | "transform-react-jsx", 7 | {} 8 | ] 9 | ], 10 | "presets": [ 11 | [ 12 | "env", 13 | { 14 | "targets": { 15 | "node": "current" 16 | } 17 | } 18 | ] 19 | ] 20 | } -------------------------------------------------------------------------------- /reactive-react/index.js: -------------------------------------------------------------------------------- 1 | import { createElement, createHandler } from "./element"; 2 | import { Component } from "./component"; 3 | import { renderStream } from "./reconciler" 4 | import { mount } from "./scheduler"; 5 | 6 | export default { 7 | renderStream, 8 | createElement, 9 | createHandler, 10 | Component, 11 | mount 12 | }; 13 | 14 | export { createElement, createHandler, Component, 15 | renderStream, 16 | mount }; 17 | -------------------------------------------------------------------------------- /prototypeAPI.js: -------------------------------------------------------------------------------- 1 | 2 | class Abc extends Component { 3 | source($) { 4 | return Observable.of(1) 5 | } 6 | render($) { 7 | return 8 | } 9 | } 10 | 11 | class LabeledSlider extends Component { 12 | constructor() { 13 | this.myRef = Creat.createRef(); 14 | } 15 | source($) { 16 | return this.myRef() 17 | } 18 | render(value, prop$) { 19 | return 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [Settings] 2 | ID = "reactive-react" 3 | 4 | # for more: https://www.netlify.com/docs/netlify-toml-reference/ 5 | 6 | [build] 7 | # This is the directory to change to before starting a build. 8 | # base = "project/" 9 | # NOTE: This is where we will look for package.json/.nvmrc/etc, not root. 10 | # This is the directory that you are publishing from (relative to root of your repo) 11 | publish = "demos/dist/" 12 | # This will be your default build command 13 | command = "npm run demo:build" 14 | # This is where we will look for your lambda functions 15 | # functions = "project/functions/" -------------------------------------------------------------------------------- /demos/components/index.js: -------------------------------------------------------------------------------- 1 | import Timer from './timer' 2 | import Blink from './blink' 3 | import Counter, {Counters} from './counter' 4 | import CrappyBird from './crappybird' 5 | import Source, {SourceSwitching} from './sourceswitching' 6 | import CrazyCharts from '../time-slicing' 7 | 8 | // barrel rollup of all neighbors 9 | // export Timer from './timer' 10 | // export Blink from './blink' 11 | // export Counter, {Counters} from './counter' 12 | // export CrappyBird from './crappybird' 13 | // export Source, {SourceSwitching} from './sourceswitching' 14 | export {Timer, Blink, Counter, Counters, CrappyBird, Source, SourceSwitching, CrazyCharts} -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // Rollup plugins 2 | import babel from 'rollup-plugin-babel'; 3 | 4 | export default { 5 | entry: 'reactive-react/index.js', 6 | dest: 'build/reactive-react.js', 7 | format: 'iife', 8 | sourceMap: 'inline', 9 | moduleName: 'reactive-react', 10 | plugins: [ 11 | babel({ 12 | babelrc: false, 13 | presets: [['env', { modules: false }]], 14 | plugins: [ 15 | "transform-object-rest-spread", 16 | "transform-class-properties", 17 | [ 18 | "transform-react-jsx", 19 | {} 20 | ] 21 | ], 22 | exclude: 'node_modules/**', 23 | }), 24 | ], 25 | }; -------------------------------------------------------------------------------- /demos/components/timer.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | export default class Timer extends Component { 7 | // demonstrate interval time 8 | initialState = 0 9 | source($) { 10 | const source = Interval(this.props.ms) // tick every second 11 | const reducer = x => x + 0.5 // count up 12 | // source returns an observable 13 | return scan(source, reducer, 0) // from zero 14 | } 15 | render(state, stateMap) { 16 | return
number of seconds elapsed: {state}
17 | } 18 | } -------------------------------------------------------------------------------- /reactive-react/updateProperties.js: -------------------------------------------------------------------------------- 1 | export function updateDomProperties(dom, prevProps, nextProps) { 2 | const isEvent = name => name.startsWith("on"); 3 | const isAttribute = name => !isEvent(name) && name != "children"; 4 | 5 | // Remove event listeners 6 | Object.keys(prevProps).filter(isEvent).forEach(name => { 7 | const eventType = name.toLowerCase().substring(2); 8 | dom.removeEventListener(eventType, prevProps[name]); 9 | }); 10 | 11 | // Remove attributes 12 | Object.keys(prevProps).filter(isAttribute).forEach(name => { 13 | dom[name] = null; 14 | }); 15 | 16 | // Set attributes 17 | Object.keys(nextProps).filter(isAttribute).forEach(name => { 18 | dom[name] = nextProps[name]; 19 | }); 20 | 21 | // Add event listeners 22 | Object.keys(nextProps).filter(isEvent).forEach(name => { 23 | const eventType = name.toLowerCase().substring(2); 24 | dom.addEventListener(eventType, nextProps[name]); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /reactive-react/element.js: -------------------------------------------------------------------------------- 1 | import Observable from 'zen-observable' 2 | import { createChangeEmitter } from 'change-emitter' 3 | 4 | export const TEXT_ELEMENT = "TEXT ELEMENT"; 5 | 6 | export function createElement(type, config, ...args) { 7 | const props = Object.assign({}, config); 8 | const hasChildren = args.length > 0; 9 | const rawChildren = hasChildren ? [].concat(...args) : []; 10 | props.children = rawChildren 11 | .filter(c => c != null && c !== false) 12 | .map(c => c instanceof Object ? c : createTextElement(c)); 13 | return { type, props }; 14 | } 15 | 16 | function createTextElement(value) { 17 | return createElement(TEXT_ELEMENT, { nodeValue: value }); 18 | } 19 | 20 | export function createHandler(_fn) { 21 | const emitter = createChangeEmitter() 22 | let handler = x => { 23 | emitter.emit(x) 24 | } 25 | handler.$ = new Observable(observer => { 26 | return emitter.listen(value => { 27 | observer.next(_fn ? _fn(value) : value) 28 | } 29 | ) 30 | }) 31 | return handler 32 | } -------------------------------------------------------------------------------- /demos/components/sourceswitching.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | import Timer from './timer' 7 | import Counter from './counter' 8 | 9 | export class SourceSwitching extends Component { 10 | initialState = true 11 | toggle = createHandler() 12 | source($) { 13 | const source = this.toggle.$ 14 | const reducer = x => !x 15 | return {source, reducer} 16 | } 17 | render(state, stateMap) { 18 | return
19 | 20 | { 21 | state ? this.props.left : this.props.right 22 | } 23 |
24 | } 25 | } 26 | 27 | export default function Source() { 28 | // demonstrate ability to switch sources 29 | return } 31 | left={} 32 | right={} 33 | /> 34 | } -------------------------------------------------------------------------------- /demos/components/blink.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | export default class Blink extends Component { 7 | // more fun time demo 8 | // initialState = true // proper 9 | initialState = 0 // hacky 10 | source($) { 11 | // there is a bug right now where switching sources subscribes to the new source twice 12 | // havent been able to chase it down so i had to do this hacky thing 13 | // i'm sorry :( breaking demos last minute before talk sucks 14 | // const reducer = x => !x // the proper reducer 15 | const reducer = (acc, x) => acc + x // hacky reducer 16 | const source = Interval(500, 1) // tick every second 17 | return {source, reducer} 18 | } 19 | render(state) { 20 | const style = { 21 | visibility: ((state/2 + 1) % 2) ? // hacky 22 | 'visible' : 23 | 'hidden'} 24 | return
Bring back the blink tag!
25 | } 26 | } -------------------------------------------------------------------------------- /demos/components/counter.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | import './button.css' 7 | 8 | export default class Counter extends Component { 9 | // demonstrate basic counter 10 | initialState = 0 11 | increment = createHandler(e => 1) 12 | // decrement = createHandler(e => -1) 13 | source($) { 14 | // const source = merge(this.increment.$, this.decrement.$) 15 | const source = this.increment.$ 16 | const reducer = (acc, n) => acc + n 17 | return {source, reducer} 18 | } 19 | render(state, stateMap) { 20 | const {name = "counter"} = this.props 21 | return
22 |

23 | {name}: {state} 24 |

25 | + 26 | {/* - */} 27 |
28 | } 29 | } 30 | 31 | 32 | export function Counters() { 33 | // demonstrate independent states 34 | return
35 | 36 | 37 |
38 | } -------------------------------------------------------------------------------- /reactive-react/component.js: -------------------------------------------------------------------------------- 1 | // import { reconcile } from "./reconciler"; 2 | import {Interval, scan, startWith, merge, mapToConstant} from './swyxjs' 3 | 4 | export class Component { 5 | constructor(props) { 6 | this.props = props; 7 | this.state = this.state || {}; 8 | } 9 | 10 | // setState(partialState) { 11 | // this.state = Object.assign({}, this.state, partialState); 12 | // updateInstance(this.__internalInstance); 13 | // } 14 | 15 | // class method because it feeds in this.initialState 16 | combineReducer(obj) { 17 | const sources = Object.entries(obj).map(([k,fn]) => { 18 | let subReducer = fn(obj) 19 | // there are two forms of return the subreducer can have 20 | // straight stream form 21 | // or object form where we need to scan it into string 22 | if (subReducer.source) { // object form 23 | subReducer = scan(subReducer.source, 24 | subReducer.reducer || ((_, n) => n), 25 | this.initialState[k] 26 | ) 27 | } 28 | return subReducer 29 | .map(x => ({[k]: x})) // map to its particular namespace 30 | }) 31 | const source = merge(...sources) 32 | const reducer = (acc, n) => ({...acc, ...n}) 33 | return {source, reducer} 34 | } 35 | } 36 | 37 | // function updateInstance(internalInstance) { 38 | // const parentDom = internalInstance.dom.parentNode; 39 | // const element = internalInstance.element; 40 | // reconcile(parentDom, internalInstance, element); 41 | // } 42 | 43 | export function createPublicInstance(element/*, internalInstance*/) { 44 | const { type, props } = element; 45 | const publicInstance = new type(props); 46 | // publicInstance.__internalInstance = internalInstance; 47 | return publicInstance; 48 | } 49 | -------------------------------------------------------------------------------- /demos/eleventy.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0}::-moz-selection{background-color:#222;color:#067ada}::selection{background-color:#222;color:#067ada}html{background-color:#222}body{text-align:center;background-color:#067ada;color:#cde4f8;font-family:sans-serif;font-weight:30;font-size:16px;line-height:1.8}@media (min-width: 740px){body{font-size:18px}}.container{margin-left:auto;margin-right:auto;width:90%;text-align:left}h1,h2,h3{color:#fff;font-weight:600;margin-top:2.5em;margin-bottom:1em;line-height:1em}h1{margin-top:0.4em;font-size:4em;line-height:0.9em}@media (min-width: 740px){h1{font-size:5em}}@media (min-width: 1200px){h1{font-size:6em}}p{margin-bottom:1em}.subtitle{margin-top:-3em;margin-bottom:6em}ul,ol{padding-left:1em}a:link,a:visited{color:#222;text-decoration:none;border-bottom:solid 1px #1f87de}a:hover,a:focus{color:#fff;border-bottom:solid 1px #222}.nav{padding-top:3em}.nav li{display:inline}.nav a:link,.nav a:visited{display:inline-block;border-top:solid 1px #9bcaf0;border-bottom-style:none;padding-top:0.8em;padding-bottom:2em;margin-left:0;margin-right:0.7em;width:8em;text-align:left;color:#cde4f8;text-decoration:none}.nav a:hover,.nav a:focus{color:#fff;border-top:solid 1px #222;border-bottom-style:none}.nav small{display:block;font-size:0.7em;color:#9bcaf0}code{font-size:0.8em;background-color:#0971c8;color:#fff;padding-top:2px;padding-bottom:2px;padding-left:4px;padding-right:4px}pre{margin-top:2em;margin-bottom:2em;padding-top:1em;padding-bottom:1em;padding-left:2em;padding-right:2em;line-height:1.2;background-color:#0971c8;border:solid 1px #0e60a3;overflow:auto}pre code{border-style:none;padding-top:0;padding-bottom:0;padding-left:0;padding-right:0}footer{margin-top:6em;padding-top:4em;padding-bottom:6em;border-top:solid 1px #1f87de;font-size:0.7em;color:#6aafe9}footer a:link,footer a:visited{color:#b4d7f4;border-bottom:solid 1px #1a3c59}footer a:hover,footer a:focus{color:#222;border-bottom:solid 1px #fff} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swyx/reactive-react", 3 | "version": "1.0.2", 4 | "license": "MIT", 5 | "dependencies": { 6 | "change-emitter": "^0.1.6", 7 | "d3": "^5.5.0", 8 | "virtual-dom": "^2.1.1", 9 | "zen-observable": "^0.8.9" 10 | }, 11 | "main": "build/reactive-react.umd.js", 12 | "module": "build/reactive-react.es.js", 13 | "files": [ 14 | "build" 15 | ], 16 | "devDependencies": { 17 | "ava": "^0.19.0", 18 | "babel-cli": "^6.24.1", 19 | "babel-plugin-transform-class-properties": "^6.24.1", 20 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 21 | "babel-plugin-transform-react-jsx": "^6.24.1", 22 | "babel-preset-env": "^1.3.3", 23 | "babel-register": "^6.24.1", 24 | "browser-env": "^2.0.29", 25 | "npm-run-all": "^4.0.2", 26 | "parcel-bundler": "^1.9.7", 27 | "rollup": "^0.41.6", 28 | "rollup-plugin-babel": "^3.0.7" 29 | }, 30 | "scripts": { 31 | "start": "npm run demo:start", 32 | "demo:start": "parcel demos/index.html --no-hmr", 33 | "demo:build": "parcel build demos/index.html -d demos/dist", 34 | "build:deptree": "depcruise --exclude '^node_modules' --output-type dot reactive-react | dot -T svg > dependencygraph.svg", 35 | "build:module": "rollup -c -f es -n reactive-react -o build/reactive-react.es.js", 36 | "build:main": "rollup -c -f umd -n reactive-react -o build/reactive-react.umd.js", 37 | "build": "run-p build:module build:main build:deptree", 38 | "prepublishOnly": "npm run build" 39 | }, 40 | "babel": { 41 | "plugins": [ 42 | "transform-object-rest-spread", 43 | "transform-class-properties", 44 | [ 45 | "transform-react-jsx", 46 | {} 47 | ] 48 | ], 49 | "presets": [ 50 | [ 51 | "env", 52 | { 53 | "targets": { 54 | "node": "current" 55 | } 56 | } 57 | ] 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /demos/time-slicing/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import {fromEvent} from '../../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | import _ from 'lodash'; 7 | import Charts from './Charts'; 8 | // import Clock from './Clock'; 9 | import './index.css'; 10 | 11 | let cachedData = new Map(); 12 | 13 | export default class App extends Component { 14 | initialState = '' 15 | 16 | // Random data for the chart 17 | getStreamData(input) { 18 | if (cachedData.has(input)) { 19 | return cachedData.get(input); 20 | } 21 | const multiplier = input.length !== 0 ? input.length : 1; 22 | const complexity = 23 | (parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25; 24 | const data = _.range(5).map(t => 25 | _.range(complexity * multiplier).map((j, i) => { 26 | return { 27 | x: j, 28 | y: (t + 1) * _.random(0, 255), 29 | }; 30 | }) 31 | ); 32 | cachedData.set(input, data); 33 | return data; 34 | } 35 | 36 | handleChange = createHandler(e => e.target.value) 37 | source() { 38 | return { 39 | source: this.handleChange.$, 40 | reducer: (_, x) => x 41 | } 42 | } 43 | 44 | render(state, stateMap) { 45 | const Wrapper = 'div' 46 | const data = this.getStreamData(state); 47 | return ( 48 |
49 | 56 | 57 |
58 | 59 |
60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | 67 | const container = document.getElementById('app'); 68 | mount(, container); 69 | -------------------------------------------------------------------------------- /reactive-react/swyxjs.js: -------------------------------------------------------------------------------- 1 | import Observable from 'zen-observable' 2 | export { merge, combineLatest, zip } from 'zen-observable/extras' 3 | 4 | export function Interval(tick = 1000, tickData = Symbol('tick')) { 5 | return new Observable(observer => { 6 | let timer = () => setTimeout(() => { 7 | if (typeof tickData === 'function') tickData = tickData() 8 | observer.next(tickData); 9 | timer() 10 | // observer.complete(); 11 | }, tick); 12 | timer() 13 | 14 | // On unsubscription, cancel the timer 15 | return () => clearTimeout(timer); 16 | 17 | }) 18 | } 19 | 20 | export function fromEvent(el, eventType) { 21 | return new Observable(observer => { 22 | const handler = e => observer.next(e) 23 | el.addEventListener(eventType, handler) 24 | // on unsub, remove event listener 25 | return () => el.removeEventListener(eventType, handler) 26 | }) 27 | } 28 | 29 | const NOINIT = Symbol('NO_INITIAL_VALUE') 30 | export function scan(obs, cb, seed = NOINIT) { 31 | let sub, acc = seed, hasValue = false 32 | const hasSeed = acc !== NOINIT 33 | return new Observable(observer => { 34 | sub = obs.subscribe(value => { 35 | if (observer.closed) return 36 | let first = !hasValue; 37 | hasValue = true 38 | if (!first || hasSeed ) { 39 | try { acc = cb(acc, value) } 40 | catch (e) { return observer.error(e) } 41 | observer.next(acc); 42 | } 43 | else { 44 | acc = value 45 | } 46 | }) 47 | return sub 48 | }) 49 | } 50 | 51 | // Flatten a collection of observables and only output the newest from each 52 | export function switchLatest(higherObservable) { 53 | return new Observable(observer => { 54 | let currentObs = null 55 | return higherObservable.subscribe({ 56 | next(obs) { 57 | if (currentObs) currentObs.unsubscribe() // unsub and switch 58 | currentObs = obs.subscribe(observer.subscribe) 59 | }, 60 | error(e) { 61 | observer.error(e) // untested 62 | }, 63 | complete() { 64 | // i dont think it should complete? 65 | // observer.complete() 66 | } 67 | }) 68 | }); 69 | } 70 | 71 | export function mapToConstant(obs, val) { 72 | return new Observable(observer => { 73 | const handler = obs.subscribe(() => observer.next(val)) 74 | return handler 75 | }) 76 | } 77 | 78 | export function startWith(obs, val) { 79 | return new Observable(observer => { 80 | observer.next(val) // immediately output this value 81 | const handler = obs.subscribe(x => observer.next(x)) 82 | return () => handler() 83 | }) 84 | } -------------------------------------------------------------------------------- /demos/time-slicing/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0px; 4 | margin: 0px; 5 | user-select: none; 6 | font-family: Karla, Helvetica Neue, Helvetica, sans-serif; 7 | background: rgb(34, 34, 34); 8 | color: white; 9 | overflow: hidden; 10 | } 11 | 12 | .VictoryContainer { 13 | opacity: 0.8; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | #root { 21 | height: 100vh; 22 | } 23 | 24 | .container { 25 | width: 100%; 26 | max-width: 960px; 27 | margin: auto; 28 | padding: 10px; 29 | } 30 | 31 | .rendering { 32 | margin-top: 20px; 33 | margin-bottom: 20px; 34 | zoom: 1.8; 35 | } 36 | 37 | label { 38 | zoom: 1; 39 | margin-right: 50px; 40 | font-size: 30px; 41 | } 42 | 43 | label.selected { 44 | font-weight: bold; 45 | } 46 | 47 | label:nth-child(1).selected { 48 | color: rgb(253, 25, 153); 49 | } 50 | 51 | label:nth-child(2).selected { 52 | color: rgb(255, 240, 1); 53 | } 54 | 55 | label:nth-child(3).selected { 56 | color: #61dafb; 57 | } 58 | 59 | .chart { 60 | width: 100%; 61 | height: 100%; 62 | } 63 | 64 | .input { 65 | padding: 16px; 66 | font-size: 30px; 67 | width: 100%; 68 | display: block; 69 | } 70 | .input.sync { 71 | outline-color: rgba(253, 25, 153, 0.1); 72 | } 73 | .input.debounced { 74 | outline-color: rgba(255, 240, 1, 0.1); 75 | } 76 | .input.async { 77 | outline-color: rgba(97, 218, 251, 0.1); 78 | } 79 | 80 | 81 | label { 82 | font-size: 20px; 83 | } 84 | 85 | label label { 86 | display: 'inline-block'; 87 | margin-left: 20px; 88 | } 89 | 90 | .row { 91 | flex: 1; 92 | display: flex; 93 | margin-top: 20px; 94 | min-height: 100%; 95 | } 96 | 97 | .column { 98 | flex: 1; 99 | } 100 | 101 | .demo { 102 | position: relative; 103 | min-height: 100vh; 104 | } 105 | 106 | .stutterer { 107 | transform: scale(1.5); 108 | height: 310px; 109 | width: 310px; 110 | position: absolute; 111 | left: 0; 112 | right: 0; 113 | top: -256px; 114 | bottom: 0; 115 | margin: auto; 116 | box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.2); 117 | border-radius: 200px; 118 | } 119 | 120 | .clockHand { 121 | stroke: white; 122 | stroke-width: 10px; 123 | stroke-linecap: round; 124 | } 125 | 126 | .clockFace { 127 | stroke: white; 128 | stroke-width: 10px; 129 | } 130 | 131 | .arcHand { 132 | } 133 | 134 | .innerLine { 135 | border-radius: 6px; 136 | position: absolute; 137 | height: 149px; 138 | left: 47.5%; 139 | top: 0%; 140 | width: 5%; 141 | background-color: red; 142 | transform-origin: bottom center; 143 | } 144 | -------------------------------------------------------------------------------- /demos/components/crappybird.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import {Interval, scan, startWith, merge, mapToConstant} from '../../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | export default class CrappyBird extends Component { 7 | // merging time and coutner 8 | initialState = { 9 | input: 50, 10 | target: 50 11 | } 12 | increment = createHandler(e => 3) 13 | source($) { 14 | return this.combineReducer({ 15 | input: () => { 16 | const source = merge(this.increment.$, Interval(200,-1)) 17 | const reducer = (acc, x) => Math.max(0,acc + x) 18 | return {source, reducer} 19 | }, 20 | target: () => { 21 | const source = Interval(200) 22 | const reducer = (acc) => { 23 | const int = acc + Math.random() * 8 - 4 24 | return int - (int-50)/40 // bias toward 50 25 | } 26 | return {source, reducer} 27 | } 28 | }) 29 | } 30 | render(state, stateMap) { 31 | const {input, target} = state 32 | return
33 |

Crappy Bird: Match the bird to the target!

34 |
35 |
{ Math.abs(input - target) < 20 ? '👍 Good 👍' : '☠️ LOSING! ☠️'}
36 |
Bird
37 |
Target
38 |
39 | {/* */} 40 | + 41 |
42 |
43 |
44 |
45 | 💩 46 |
47 |
48 |
49 |
50 |
51 |
52 | 🎯 53 |
54 |
55 |
56 |
57 |
58 | } 59 | } 60 | 61 | 62 | // {/*

Bird:

63 | //

Target:

*/} -------------------------------------------------------------------------------- /reactive-react/scheduler.js: -------------------------------------------------------------------------------- 1 | import Observable from 'zen-observable' 2 | import {fromEvent, scan, merge, startWith, switchLatest} from './swyxjs' 3 | import diff from 'virtual-dom/diff'; 4 | import patch from 'virtual-dom/patch'; 5 | import createElement from 'virtual-dom/create-element'; 6 | import { createChangeEmitter } from 'change-emitter' 7 | import { renderStream } from './reconciler' 8 | 9 | export const stateMapPointer = new Map() 10 | 11 | const circuitBreakerflag = !true // set true to enable debugger in infinite loops 12 | let circuitBreaker = -200 13 | 14 | const emitter = createChangeEmitter() 15 | // single UI thread; this is the observable that sticks around and swaps out source 16 | const UIthread = new Observable(observer => { 17 | emitter.listen(x => { 18 | observer.next(x) 19 | }) 20 | }) 21 | // mount the vdom on to the dom and 22 | // set up the runtime from sources and 23 | // patch the vdom 24 | // --- 25 | // returns an unsubscribe method you can use to unmount 26 | export function mount(rootElement, container) { 27 | // initial, throwaway-ish frame 28 | let {source, instance} = renderStream(rootElement, {}, undefined, stateMapPointer) 29 | let instancePointer = instance 30 | const rootNode = createElement(instance.dom) 31 | const containerChild = container.firstElementChild 32 | if (containerChild) { 33 | container.replaceChild(rootNode,containerChild) // hot reloaded mount 34 | } else { 35 | container.appendChild(rootNode) // initial mount 36 | } 37 | let currentSrc$ = null 38 | let SoS = startWith(UIthread, source) // stream of streams 39 | return SoS.subscribe( 40 | src$ => { // this is the current sourceStream we are working with 41 | if (currentSrc$) console.log('unsub!') || currentSrc$.unsubscribe() // unsub from old stream 42 | if (circuitBreakerflag && circuitBreaker++ > 0) debugger 43 | /**** main */ 44 | const source2$ = scan( 45 | src$, 46 | ({instance, stateMap}, nextState) => { 47 | const streamOutput = renderStream(rootElement, instance, nextState, stateMap) 48 | if (streamOutput.isNewStream) { // quick check 49 | const nextSource$ = streamOutput.source 50 | instancePointer = streamOutput.instance 51 | patch(rootNode, diff(instance.dom, instancePointer.dom)) // render to screen 52 | emitter.emit(nextSource$) // update the UI thread; source will switch 53 | } else { 54 | const nextinstance = streamOutput.instance 55 | patch(rootNode, diff(instance.dom, nextinstance.dom)) // render to screen 56 | return {instance: nextinstance, stateMap: stateMap} 57 | } 58 | }, 59 | {instance: instancePointer, stateMap: stateMapPointer} // accumulator 60 | ) 61 | /**** end main */ 62 | currentSrc$ = 63 | source2$ 64 | .subscribe() 65 | } 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /demos/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../reactive-react' 3 | import {Interval, scan, startWith, merge, mapToConstant} from '../reactive-react/swyxjs' 4 | import Observable from 'zen-observable' 5 | 6 | import {Timer, Blink, Counter, Counters, CrappyBird, Source, SourceSwitching, CrazyCharts} from './components' 7 | 8 | import './eleventy.css' 9 | import './custom.css' 10 | 11 | class App extends Component { 12 | initialState = "counter" 13 | router = createHandler(e => e.target.name) 14 | source($) { 15 | return this.router.$ 16 | } 17 | render(state) { 18 | const Display = { 19 | 'counter': () => , 20 | // 'timer': () => , 21 | 'blink': () => , 22 | 'crappy': () => , 23 | 'charts': () => , 24 | }[state] || (() => ) 25 | 26 | function selectedLink(name) { 27 | return { 28 | name, 29 | style: state === name && {color: 'yellow'} 30 | } 31 | } 32 | return
33 |
34 |
reactive-react Demo: {state}
35 |
36 | @swyx 37 | ☢️ 38 | Slides 39 | Github 40 |
41 |
42 |
43 | 60 |
61 | {Display()} 62 |
63 | By swyx 64 |
65 |
66 | } 67 | } 68 | 69 | mount(, document.getElementById('app')) 70 | -------------------------------------------------------------------------------- /demos/time-slicing/Clock.js: -------------------------------------------------------------------------------- 1 | // /** @jsx createElement */ 2 | // import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | 4 | // const SPEED = 0.003 / Math.PI; 5 | // const FRAMES = 10; 6 | 7 | // export default class Clock extends PureComponent { 8 | // faceRef = createRef(); 9 | // arcGroupRef = createRef(); 10 | // clockHandRef = createRef(); 11 | // frame = null; 12 | // hitCounter = 0; 13 | // rotation = 0; 14 | // t0 = Date.now(); 15 | // arcs = []; 16 | 17 | // animate = () => { 18 | // const now = Date.now(); 19 | // const td = now - this.t0; 20 | // this.rotation = (this.rotation + SPEED * td) % (2 * Math.PI); 21 | // this.t0 = now; 22 | 23 | // this.arcs.push({rotation: this.rotation, td}); 24 | 25 | // let lx, ly, tx, ty; 26 | // if (this.arcs.length > FRAMES) { 27 | // this.arcs.forEach(({rotation, td}, i) => { 28 | // lx = tx; 29 | // ly = ty; 30 | // const r = 145; 31 | // tx = 155 + r * Math.cos(rotation); 32 | // ty = 155 + r * Math.sin(rotation); 33 | // const bigArc = SPEED * td < Math.PI ? '0' : '1'; 34 | // const path = `M${tx} ${ty}A${r} ${r} 0 ${bigArc} 0 ${lx} ${ly}L155 155`; 35 | // const hue = 120 - Math.min(120, td / 4); 36 | // const colour = `hsl(${hue}, 100%, ${60 - i * (30 / FRAMES)}%)`; 37 | // if (i !== 0) { 38 | // const arcEl = this.arcGroupRef.current.children[i - 1]; 39 | // arcEl.setAttribute('d', path); 40 | // arcEl.setAttribute('fill', colour); 41 | // } 42 | // }); 43 | // this.clockHandRef.current.setAttribute('d', `M155 155L${tx} ${ty}`); 44 | // this.arcs.shift(); 45 | // } 46 | 47 | // if (this.hitCounter > 0) { 48 | // this.faceRef.current.setAttribute( 49 | // 'fill', 50 | // `hsla(0, 0%, ${this.hitCounter}%, 0.95)` 51 | // ); 52 | // this.hitCounter -= 1; 53 | // } else { 54 | // this.hitCounter = 0; 55 | // this.faceRef.current.setAttribute('fill', 'hsla(0, 0%, 5%, 0.95)'); 56 | // } 57 | 58 | // this.frame = requestAnimationFrame(this.animate); 59 | // }; 60 | 61 | // componentDidMount() { 62 | // this.frame = requestAnimationFrame(this.animate); 63 | // if (this.faceRef.current) { 64 | // this.faceRef.current.addEventListener('click', this.handleClick); 65 | // } 66 | // } 67 | 68 | // componentDidUpdate() { 69 | // console.log('componentDidUpdate()', this.faceRef.current); 70 | // } 71 | 72 | // componentWillUnmount() { 73 | // this.faceRef.current.removeEventListener('click', this.handleClick); 74 | // if (this.frame) { 75 | // cancelAnimationFrame(this.frame); 76 | // } 77 | // } 78 | 79 | // handleClick = e => { 80 | // e.stopPropagation(); 81 | // this.hitCounter = 50; 82 | // }; 83 | 84 | // render() { 85 | // const paths = new Array(FRAMES); 86 | // for (let i = 0; i < FRAMES; i++) { 87 | // paths.push(); 88 | // } 89 | // return ( 90 | //
91 | // 92 | // 100 | // {paths} 101 | // 102 | // 103 | //
104 | // ); 105 | // } 106 | // } 107 | -------------------------------------------------------------------------------- /reactive-react/reconciler.js: -------------------------------------------------------------------------------- 1 | import Observable from 'zen-observable' 2 | import {fromEvent, scan, merge, startWith, switchLatest} from './swyxjs' 3 | // import { updateDomProperties } from "./updateProperties"; 4 | import { TEXT_ELEMENT } from "./element"; 5 | import { createPublicInstance } from "./component"; 6 | import h from 'virtual-dom/h' 7 | // import VNode from "virtual-dom/vnode/vnode" 8 | import VText from "virtual-dom/vnode/vtext" 9 | 10 | const circuitBreakerflag = !true // set true to enable debugger in infinite loops 11 | let circuitBreaker = -500 12 | // traverse all children and collect a stream of all sources 13 | // AND render. a bit of duplication, but we get persistent instances which is good 14 | export function renderStream(element, instance, state, stateMap) { 15 | // this is a separate function because scope gets messy when being recursive 16 | let isNewStream = false // assume no stream switching by default 17 | // this is the first ping of data throughout the app 18 | let source = Observable.of(state) 19 | const addToStream = _source => { 20 | // visit each source and merge with source 21 | if (_source) return source = merge(source, _source) 22 | } 23 | const markNewStream = () => isNewStream = true 24 | const newInstance = render(source, addToStream, markNewStream)(element, instance, state, stateMap) 25 | return {source, instance: newInstance, isNewStream} 26 | } 27 | 28 | /** core render logic */ 29 | export function render(source, addToStream, markNewStream) { // this is the nonrecursive part 30 | return function renderWithStream(element, instance, state, stateMap) { // recursive part 31 | let newInstance 32 | const { type, props } = element 33 | 34 | const isDomElement = typeof type === "string"; 35 | if (circuitBreakerflag && circuitBreaker++ > 0) debugger 36 | const {children = [], ...rest} = props 37 | if (isDomElement) { 38 | const childInstances = children.map( 39 | (el, i) => { 40 | // ugly but necessary to allow functional children 41 | // mapping element's children to instance's childInstances 42 | const _childInstances = instance && (instance.childInstance || instance.childInstances[i]) 43 | return renderWithStream( // recursion 44 | el, 45 | _childInstances, 46 | state, 47 | stateMap) 48 | } 49 | ); 50 | const childDoms = childInstances.map(childInstance => childInstance.dom); 51 | let lcaseProps = {} 52 | Object.entries(rest).forEach(([k, v]) => lcaseProps[formatProps(k)] = v) 53 | const dom = type === TEXT_ELEMENT 54 | ? new VText(props.nodeValue) 55 | : h(type, lcaseProps, childDoms); // equivalent of appendchild 56 | newInstance = { dom, element, childInstances }; 57 | } else { // component element 58 | let publicInstance 59 | // might have to do more diffing of props in future; further research needed 60 | // used to compare instance.element === element; this proved too easy to be false so went for types 61 | if (instance && instance.publicInstance && instance.element.type === element.type) { 62 | // just reuse old instance if it already exists 63 | publicInstance = instance && instance.publicInstance 64 | } else { 65 | markNewStream() // mark as dirty in parent scope; will rerender 66 | if (circuitBreakerflag && circuitBreaker++ > 0) debugger 67 | if (stateMap.has(publicInstance)) stateMap.delete(publicInstance) 68 | publicInstance = createPublicInstance(element); 69 | } 70 | let localState = stateMap.get(publicInstance) 71 | if (localState === undefined) localState = publicInstance.initialState 72 | publicInstance.state = localState // for access with this.state 73 | if (Object.keys(rest).length) publicInstance.props = rest // update with new props // TODO: potentially buggy 74 | // console.log({rest}) 75 | if (publicInstance.source) { 76 | const src = publicInstance.source(source) 77 | // there are two forms of Component.source 78 | const src$ = src.reducer && publicInstance.initialState !== undefined ? 79 | // 1. the reducer form 80 | scan(src.source, src.reducer, publicInstance.initialState) : 81 | // 2. and raw stream form 82 | src 83 | addToStream(src$ 84 | .map(event => { 85 | stateMap.set(publicInstance, event) 86 | return {instance: publicInstance, event} // tag it to the instance 87 | }) 88 | ); 89 | } 90 | const childElement = publicInstance.render ? 91 | publicInstance.render(localState, stateMap) : 92 | publicInstance; 93 | 94 | const childInstance = renderWithStream(childElement, instance && instance.childInstance, state, stateMap) 95 | const dom = childInstance.dom 96 | newInstance = { dom, element, childInstance, publicInstance } 97 | } 98 | return newInstance 99 | } 100 | } 101 | 102 | function formatProps(k) { 103 | if (k.startsWith('on')) return k.toLowerCase() 104 | return k 105 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a prototype mockup of a "Reactive" version of React. Do not use unless you are swyx. 2 | 3 | ## Watch the React Rally talk 4 | 5 | Talk video: https://www.youtube.com/watch?v=nyFHR0dDZo0 6 | 7 | TL;DR with writeup: https://www.swyx.io/ReactRally 8 | 9 | ## Description 10 | 11 | **Note: we are aware of the double subscribe bug when a source is switched. didnt have time to figure out the fix before react rally. Blink tag example has a hacky patch for this.** 12 | 13 | In this alternate universe, Observables became a part of Javascript. 14 | 15 | We take a minimal implementation of Observables, zen-observable. 16 | 17 | 18 | # Try it out 19 | 20 | `yarn start` to run the demo locally 21 | 22 | # The `reactive-react` API 23 | 24 | The React API we are targeting looks something like this (see `/demos` for actual examples): 25 | 26 | ```js 27 | class Counter extends Component { 28 | // demonstrate basic counter 29 | initialState = 0 30 | increment = createHandler(e => 1) 31 | decrement = createHandler(e => -1) 32 | source($) { 33 | const source = merge(this.increment.$, this.decrement.$) 34 | const reducer = (acc, n) => acc + n 35 | return {source, reducer} 36 | } 37 | render(state, stateMap) { 38 | const {name = "counter"} = this.props 39 | return
40 | {name}: {state} 41 | 42 | 43 |
44 | } 45 | } 46 | 47 | // taking info from event handler 48 | class Echo extends Component { 49 | handler = createHandler(e => e.target.value) 50 | initialState = 'hello world' 51 | source($) { 52 | const source = this.handler.$ 53 | const reducer = (acc, n) => n 54 | return {source, reducer} 55 | } 56 | render(state, prevState) { 57 | return
58 | 59 | {state} 60 |
61 | } 62 | } 63 | 64 | class Timer extends Component { 65 | // demonstrate interval time 66 | initialState = 0 67 | source($) { 68 | const reducer = x => x + 1 // count up 69 | const source = Interval(this.props.ms) // tick every second 70 | // source returns an observable 71 | return scan(source, reducer, 0) // from zero 72 | } 73 | render(state, stateMap) { 74 | return
number of seconds elapsed: {state}
75 | } 76 | } 77 | 78 | class Blink extends Component { 79 | // more fun time demo 80 | initialState = true 81 | source($) { 82 | const reducer = x => !x 83 | // tick every ms milliseconds 84 | const source = Interval(this.props.ms) 85 | // source can also return an observable 86 | return scan(source, reducer, true) 87 | } 88 | render(state) { 89 | const style = {display: state ? 'block' : 'none'} 90 | return
Bring back the blink tag!
91 | } 92 | } 93 | 94 | class CrappyBird extends Component { 95 | // merging time and counter 96 | initialState = { 97 | input: 50, 98 | target: 50 99 | } 100 | increment = createHandler(e => 3) 101 | source($) { 102 | return this.combineReducer({ 103 | input: () => { 104 | const source = merge(this.increment.$, Interval(200,-1)) 105 | const reducer = (acc, x) => Math.max(0,acc + x) 106 | return {source, reducer} 107 | }, 108 | target: () => { 109 | const source = Interval(200) 110 | const reducer = (acc) => { 111 | const int = acc + Math.random() * 8 - 4 112 | return int - (int-50)/30 // bias toward 50 113 | } 114 | return {source, reducer} 115 | } 116 | }) 117 | } 118 | render(state, stateMap) { 119 | const {input, target} = state 120 | return
121 | 122 |

Crappy bird

123 |

Bird:

124 |

Target:

125 |
126 | } 127 | } 128 | 129 | function Counters() { 130 | // demonstrate independent states 131 | return
132 | 133 | 134 |
135 | } 136 | function Source() { 137 | // demonstrate ability to switch sources 138 | return } 140 | right={} 141 | /> 142 | } 143 | 144 | class SourceSwitching extends Component { 145 | initialState = true 146 | toggle = createHandler() 147 | source($) { 148 | const source = this.toggle.$ 149 | const reducer = x => !x 150 | return {source, reducer} 151 | } 152 | render(state, stateMap) { 153 | return
154 | 155 | { 156 | state ? this.props.left : this.props.right 157 | } 158 |
159 | } 160 | } 161 | 162 | ``` 163 | 164 | # Local development 165 | 166 | `yarn run build` and then `npm publish` (but its under my namespace @swyx/reactive-react cos someone else has the generic one) 167 | 168 | Here is the project structure: 169 | 170 | ![dependencygraph.svg](dependencygraph.svg) 171 | -------------------------------------------------------------------------------- /demos/time-slicing/Charts.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import {mount, createElement, Component, createHandler} from '../../reactive-react' 3 | import * as d3 from "d3"; 4 | 5 | const colors = ['#fff489', '#fa57c1', '#b166cc', '#7572ff', '#69a6f9']; 6 | const containerWidth = 565 / 2 7 | const containerHeight = 400 / 2 8 | 9 | 10 | export default class Charts extends Component { 11 | render() { 12 | const data = this.props.data; 13 | const container = document.getElementById("demo") 14 | if (!container) return
Type something
// initial load only 15 | bottomChart(data) 16 | leftChart(data) 17 | rightChart(data) 18 | return ( 19 |
20 |

number of datapoints: {data[0].length * 5}

21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | 39 | function responsivefy(svg) { 40 | // get container + svg aspect ratio 41 | if (!svg.node()) return 42 | var container = d3.select(svg.node().parentNode), 43 | width = parseInt(svg.style("width")), 44 | height = parseInt(svg.style("height")), 45 | aspect = width / height; 46 | 47 | // add viewBox and preserveAspectRatio properties, 48 | // and call resize so that svg resizes on inital page load 49 | svg.attr("viewBox", "0 0 " + width + " " + height) 50 | .attr("preserveAspectRatio", "xMinYMid") 51 | .call(resize); 52 | 53 | // to register multiple listeners for same event type, 54 | // you need to add namespace, i.e., 'click.foo' 55 | // necessary if you call invoke this function for multiple svgs 56 | // api docs: https://github.com/mbostock/d3/wiki/Selections#on 57 | d3.select(window).on("resize." + container.attr("id"), resize); 58 | 59 | // get width of container and resize svg to fit it 60 | function resize() { 61 | var targetWidth = parseInt(container.style("width")); 62 | svg.attr("width", targetWidth); 63 | svg.attr("height", Math.round(targetWidth / aspect)); 64 | } 65 | } 66 | 67 | function bottomChart(data) { 68 | 69 | var margin = { top: 10, right: 20, bottom: 30, left: 30 }; 70 | var width = containerWidth * 2 - margin.left - margin.right; 71 | var height = containerHeight - margin.top - margin.bottom; 72 | 73 | // really janky clearance 74 | const temp = d3.select(".bottomchart") 75 | .selectAll("svg") 76 | if (temp._groups[0] && temp._groups[0].length) temp._groups[0] 77 | .forEach((d, i) => d.parentNode.removeChild(d)) 78 | 79 | var svg = d3.select('.bottomchart') 80 | .append('svg') 81 | .attr('width', width + margin.left + margin.right) 82 | .attr('height', height + margin.top + margin.bottom) 83 | // .call(responsivefy) 84 | .append('g') 85 | .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')'); 86 | 87 | var xScale = d3.scaleLinear() 88 | .domain([ 89 | d3.min(data, co => d3.min(co, d => d.x)), 90 | d3.max(data, co => d3.max(co, d => d.x)) 91 | ]) 92 | .range([0, width]); 93 | svg 94 | .append('g') 95 | .attr('transform', `translate(0, ${height})`) 96 | .style('stroke', 'yellow') 97 | .call(d3.axisBottom(xScale).ticks(5)); 98 | 99 | var yScale = d3.scaleLinear() 100 | .domain([ 101 | d3.min(data, co => d3.min(co, d => d.y)), 102 | d3.max(data, co => d3.max(co, d => d.y)) 103 | ]) 104 | .range([height, 0]); 105 | svg 106 | .append('g') 107 | .style('stroke', 'yellow') 108 | .call(d3.axisLeft(yScale)); 109 | 110 | var area = d3.area() 111 | .x(d => xScale(d.x)) 112 | .y0(yScale(yScale.domain()[0])) 113 | .y1(d => yScale(d.y)) 114 | .curve(d3.curveCatmullRom.alpha(0.5)); 115 | 116 | svg 117 | .selectAll('.area') 118 | .data(data) 119 | .enter() 120 | .append('path') 121 | .attr('class', 'area') 122 | .attr('d', d => area(d)) 123 | .style('stroke', (d, i) => colors[i]) 124 | .style('stroke-width', 2) 125 | .style('fill', (d, i) => colors[i]) 126 | .style('fill-opacity', 0.5) 127 | } 128 | 129 | 130 | 131 | function rightChart(data) { 132 | 133 | var margin = { top: 10, right: 20, bottom: 30, left: 30 }; 134 | var width = containerWidth - margin.left - margin.right; 135 | var height = containerHeight - margin.top - margin.bottom; 136 | 137 | // really janky clearance 138 | const temp = d3.select(".rightchart") 139 | .selectAll("svg") 140 | if (temp._groups[0] && temp._groups[0].length) temp._groups[0] 141 | .forEach((d, i) => d.parentNode.removeChild(d)) 142 | 143 | var svg = d3.select('.rightchart') 144 | .append('svg') 145 | .attr('width', width + margin.left + margin.right) 146 | .attr('height', height + margin.top + margin.bottom) 147 | // .call(responsivefy) 148 | .append('g') 149 | .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')'); 150 | 151 | var xScale = d3.scaleLinear() 152 | .domain([ 153 | d3.min(data, co => d3.min(co, d => d.x)), 154 | d3.max(data, co => d3.max(co, d => d.x)) 155 | ]) 156 | .range([0, width]); 157 | svg 158 | .append('g') 159 | .attr('transform', `translate(0, ${height})`) 160 | .style('stroke', 'yellow') 161 | .call(d3.axisBottom(xScale).ticks(5)); 162 | 163 | var yScale = d3.scaleLinear() 164 | .domain([ 165 | d3.min(data, co => d3.min(co, d => d.y)), 166 | d3.max(data, co => d3.max(co, d => d.y)) 167 | ]) 168 | .range([height, 0]); 169 | svg 170 | .append('g') 171 | .style('stroke', 'yellow') 172 | .call(d3.axisLeft(yScale)); 173 | 174 | 175 | var line = d3.line() 176 | .x(d => xScale(d.x)) 177 | .y(d => yScale(d.y)) 178 | .curve(d3.curveCatmullRom.alpha(0.5)); 179 | 180 | svg 181 | .selectAll('.line') 182 | .data(data) 183 | .enter() 184 | .append('path') 185 | .attr('class', 'line') 186 | .attr('d', d => line(d)) 187 | // .style('stroke', (d, i) => ['#FF9900', '#3369E8'][i]) 188 | .style('stroke', (d, i) => colors[i]) 189 | .style('stroke-width', 2) 190 | .style('fill', 'none'); 191 | } 192 | 193 | 194 | function leftChart(data) { 195 | 196 | var margin = { top: 10, right: 20, bottom: 30, left: 30 }; 197 | var width = containerWidth - margin.left - margin.right; 198 | var height = containerHeight - margin.top - margin.bottom; 199 | 200 | // really janky clearance 201 | const temp = d3.select(".leftchart") 202 | .selectAll("svg") 203 | if (temp._groups[0] && temp._groups[0].length) temp._groups[0] 204 | .forEach((d, i) => d.parentNode.removeChild(d)) 205 | 206 | var svg = d3.select('.leftchart') 207 | .append('svg') 208 | .attr('width', width + margin.left + margin.right) 209 | .attr('height', height + margin.top + margin.bottom) 210 | // .call(responsivefy) 211 | .append('g') 212 | .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')'); 213 | 214 | var xScale = d3.scaleLinear() 215 | .domain([ 216 | d3.min(data, co => d3.min(co, d => d.x)), 217 | d3.max(data, co => d3.max(co, d => d.x)) 218 | ]) 219 | .range([0, width]); 220 | svg 221 | .append('g') 222 | .attr('transform', `translate(0, ${height})`) 223 | .style('stroke', 'yellow') 224 | .call(d3.axisBottom(xScale).ticks(5)); 225 | 226 | var yScale = d3.scaleLinear() 227 | .domain([ 228 | d3.min(data, co => d3.min(co, d => d.y)), 229 | d3.max(data, co => d3.max(co, d => d.y)) 230 | ]) 231 | .range([height, 0]); 232 | svg 233 | .append('g') 234 | .style('stroke', 'yellow') 235 | .call(d3.axisLeft(yScale)); 236 | 237 | var circlesArr = svg 238 | .selectAll('.ball') 239 | .data(data) 240 | .enter() 241 | .append('g') 242 | .each(function(d, i) { 243 | var node = d3.select(this).selectAll('g') 244 | .data(d) 245 | .enter() 246 | .append('g') 247 | .attr('class', 'ball') 248 | .attr('transform', d => { 249 | return `translate(${xScale(d.x)}, ${yScale(d.y)})`; 250 | }); 251 | node 252 | .append('circle') 253 | .attr('cx', 0) 254 | .attr('cy', 0) 255 | .attr('r', d => 5) 256 | .style('fill-opacity', 0.5) 257 | .style('fill', colors[i]); 258 | }) 259 | } -------------------------------------------------------------------------------- /dependencygraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependency-cruiser output 11 | 12 | 13 | cluster_/reactive-react 14 | 15 | reactive-react 16 | 17 | 18 | 19 | reactive-react/component.js 20 | 21 | 22 | component.js 23 | 24 | 25 | 26 | 27 | 28 | reactive-react/swyxjs.js 29 | 30 | 31 | swyxjs.js 32 | 33 | 34 | 35 | 36 | 37 | reactive-react/component.js->reactive-react/swyxjs.js 38 | 39 | 40 | 41 | 42 | 43 | reactive-react/element.js 44 | 45 | 46 | element.js 47 | 48 | 49 | 50 | 51 | 52 | reactive-react/index.js 53 | 54 | 55 | index.js 56 | 57 | 58 | 59 | 60 | 61 | reactive-react/index.js->reactive-react/component.js 62 | 63 | 64 | 65 | 66 | 67 | reactive-react/index.js->reactive-react/element.js 68 | 69 | 70 | 71 | 72 | 73 | reactive-react/reconciler.js 74 | 75 | 76 | reconciler.js 77 | 78 | 79 | 80 | 81 | 82 | reactive-react/index.js->reactive-react/reconciler.js 83 | 84 | 85 | 86 | 87 | 88 | reactive-react/scheduler.js 89 | 90 | 91 | scheduler.js 92 | 93 | 94 | 95 | 96 | 97 | reactive-react/index.js->reactive-react/scheduler.js 98 | 99 | 100 | 101 | 102 | 103 | reactive-react/reconciler.js->reactive-react/component.js 104 | 105 | 106 | 107 | 108 | 109 | reactive-react/reconciler.js->reactive-react/element.js 110 | 111 | 112 | 113 | 114 | 115 | reactive-react/reconciler.js->reactive-react/swyxjs.js 116 | 117 | 118 | 119 | 120 | 121 | reactive-react/scheduler.js->reactive-react/reconciler.js 122 | 123 | 124 | 125 | 126 | 127 | reactive-react/scheduler.js->reactive-react/swyxjs.js 128 | 129 | 130 | 131 | 132 | 133 | reactive-react/updateProperties.js 134 | 135 | 136 | updateProperties.js 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /demos/components/button.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* BonBon Buttons 1.1 by simurai.com 4 | 5 | 1.1 Added unprefixed attributes, :focus style,