diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:46:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:48:30 -0300 |
commit | 38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch) | |
tree | 453dbf70000cc5e338b06201af1eaca8343f8f73 /preact/demo | |
parent | f26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff) | |
download | node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.gz node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.bz2 node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.zip |
Diffstat (limited to 'preact/demo')
49 files changed, 3154 insertions, 0 deletions
diff --git a/preact/demo/contenteditable.js b/preact/demo/contenteditable.js new file mode 100644 index 0000000..4e913ba --- /dev/null +++ b/preact/demo/contenteditable.js @@ -0,0 +1,25 @@ +import { createElement } from 'preact'; +import { useState } from 'preact/hooks'; + +export default function Contenteditable() { + const [value, setValue] = useState("Hey there<br />I'm editable!"); + + return ( + <div> + <div> + <button onClick={() => setValue('')}>Clear!</button> + </div> + <div + style={{ + border: '1px solid gray', + padding: '8px', + margin: '8px 0', + background: 'white' + }} + contentEditable + onInput={e => setValue(e.currentTarget.innerHTML)} + dangerouslySetInnerHTML={{ __html: value }} + /> + </div> + ); +} diff --git a/preact/demo/context.js b/preact/demo/context.js new file mode 100644 index 0000000..3fe3bc0 --- /dev/null +++ b/preact/demo/context.js @@ -0,0 +1,69 @@ +// eslint-disable-next-line no-unused-vars +import { createElement, Component, createContext, Fragment } from 'preact'; +const { Provider, Consumer } = createContext(); + +class ThemeProvider extends Component { + state = { + value: this.props.value + }; + + onClick = () => { + this.setState(prev => ({ + value: + prev.value === this.props.value ? this.props.next : this.props.value + })); + }; + + render() { + return ( + <div> + <button onClick={this.onClick}>Toggle</button> + <Provider value={this.state.value}>{this.props.children}</Provider> + </div> + ); + } +} + +class Child extends Component { + shouldComponentUpdate() { + return false; + } + + render() { + return ( + <> + <p>(blocked update)</p> + {this.props.children} + </> + ); + } +} + +export default class ContextDemo extends Component { + render() { + return ( + <ThemeProvider value="blue" next="red"> + <Child> + <Consumer> + {data => ( + <div> + <p> + current theme: <b>{data}</b> + </p> + <ThemeProvider value="black" next="white"> + <Consumer> + {data => ( + <p> + current sub theme: <b>{data}</b> + </p> + )} + </Consumer> + </ThemeProvider> + </div> + )} + </Consumer> + </Child> + </ThemeProvider> + ); + } +} diff --git a/preact/demo/devtools.js b/preact/demo/devtools.js new file mode 100644 index 0000000..0484582 --- /dev/null +++ b/preact/demo/devtools.js @@ -0,0 +1,42 @@ +// eslint-disable-next-line no-unused-vars +import { + createElement, + Component, + memo, + Fragment, + Suspense, + lazy +} from 'react'; + +function Foo() { + return <div>I'm memoed</div>; +} + +function LazyComp() { + return <div>I'm (fake) lazy loaded</div>; +} + +const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); + +const Memoed = memo(Foo); + +export default class DevtoolsDemo extends Component { + render() { + return ( + <div> + <h1>memo()</h1> + <p> + <b>functional component:</b> + </p> + <Memoed /> + <h1>lazy()</h1> + <p> + <b>functional component:</b> + </p> + <Suspense fallback={<div>Loading (fake) lazy loaded component...</div>}> + <Lazy /> + </Suspense> + </div> + ); + } +} diff --git a/preact/demo/fragments.js b/preact/demo/fragments.js new file mode 100644 index 0000000..b713860 --- /dev/null +++ b/preact/demo/fragments.js @@ -0,0 +1,26 @@ +import { createElement, Component, Fragment } from 'preact'; + +export default class extends Component { + state = { number: 0 }; + + componentDidMount() { + setInterval(_ => this.updateChildren(), 1000); + } + + updateChildren() { + this.setState(state => ({ number: state.number + 1 })); + } + + render(props, state) { + return ( + <div> + <div>{state.number}</div> + <> + <div>one</div> + <div>{state.number}</div> + <div>three</div> + </> + </div> + ); + } +} diff --git a/preact/demo/index.js b/preact/demo/index.js new file mode 100644 index 0000000..72c58ee --- /dev/null +++ b/preact/demo/index.js @@ -0,0 +1,187 @@ +import { createElement, render, Component, Fragment } from 'preact'; +// import renderToString from 'preact-render-to-string'; +import './style.scss'; +import { Router, Link } from 'preact-router'; +import Pythagoras from './pythagoras'; +import Spiral from './spiral'; +import Reorder from './reorder'; +import Todo from './todo'; +import Fragments from './fragments'; +import Context from './context'; +import installLogger from './logger'; +import ProfilerDemo from './profiler'; +import KeyBug from './key_bug'; +import StateOrderBug from './stateOrderBug'; +import PeopleBrowser from './people'; +import StyledComp from './styled-components'; +import { initDevTools } from 'preact/devtools/src/devtools'; +import { initDebug } from 'preact/debug/src/debug'; +import DevtoolsDemo from './devtools'; +import SuspenseDemo from './suspense'; +import Redux from './redux'; +import TextFields from './textFields'; +import ReduxBug from './reduxUpdate'; +import SuspenseRouterBug from './suspense-router'; +import NestedSuspenseBug from './nested-suspense'; +import Contenteditable from './contenteditable'; +import { MobXDemo } from './mobx'; + +let isBenchmark = /(\/spiral|\/pythagoras|[#&]bench)/g.test( + window.location.href +); +if (!isBenchmark) { + // eslint-disable-next-line no-console + console.log('Enabling devtools and debug'); + initDevTools(); + initDebug(); +} + +// mobx-state-tree fix +window.setImmediate = setTimeout; + +class Home extends Component { + render() { + return ( + <div> + <h1>Hello</h1> + </div> + ); + } +} + +class DevtoolsWarning extends Component { + onClick = () => { + window.location.reload(); + }; + + render() { + return ( + <button onClick={this.onClick}> + Start Benchmark (disables devtools) + </button> + ); + } +} + +class App extends Component { + render({ url }) { + return ( + <div class="app"> + <header> + <nav> + <Link href="/" activeClassName="active"> + Home + </Link> + <Link href="/reorder" activeClassName="active"> + Reorder + </Link> + <Link href="/spiral" activeClassName="active"> + Spiral + </Link> + <Link href="/pythagoras" activeClassName="active"> + Pythagoras + </Link> + <Link href="/todo" activeClassName="active"> + ToDo + </Link> + <Link href="/fragments" activeClassName="active"> + Fragments + </Link> + <Link href="/key_bug" activeClassName="active"> + Key Bug + </Link> + <Link href="/profiler" activeClassName="active"> + Profiler + </Link> + <Link href="/context" activeClassName="active"> + Context + </Link> + <Link href="/devtools" activeClassName="active"> + Devtools + </Link> + <Link href="/empty-fragment" activeClassName="active"> + Empty Fragment + </Link> + <Link href="/people" activeClassName="active"> + People Browser + </Link> + <Link href="/state-order" activeClassName="active"> + State Order + </Link> + <Link href="/styled-components" activeClassName="active"> + Styled Components + </Link> + <Link href="/redux" activeClassName="active"> + Redux + </Link> + <Link href="/mobx" activeClassName="active"> + MobX + </Link> + <Link href="/suspense" activeClassName="active"> + Suspense / lazy + </Link> + <Link href="/textfields" activeClassName="active"> + Textfields + </Link> + <Link href="/reduxBug/1" activeClassName="active"> + Redux Bug + </Link> + <Link href="/suspense-router" activeClassName="active"> + Suspense Router Bug + </Link> + <Link href="/nested-suspense" activeClassName="active"> + Nested Suspense Bug + </Link> + <Link href="/contenteditable" activeClassName="active"> + contenteditable + </Link> + </nav> + </header> + <main> + <Router url={url}> + <Home path="/" /> + <StateOrderBug path="/state-order" /> + <Reorder path="/reorder" /> + <div path="/spiral"> + {!isBenchmark ? <DevtoolsWarning /> : <Spiral />} + </div> + <div path="/pythagoras"> + {!isBenchmark ? <DevtoolsWarning /> : <Pythagoras />} + </div> + <Todo path="/todo" /> + <Fragments path="/fragments" /> + <ProfilerDemo path="/profiler" /> + <KeyBug path="/key_bug" /> + <Context path="/context" /> + <DevtoolsDemo path="/devtools" /> + <SuspenseDemo path="/suspense" /> + <EmptyFragment path="/empty-fragment" /> + <PeopleBrowser path="/people/:user?" /> + <StyledComp path="/styled-components" /> + <Redux path="/redux" /> + <MobXDemo path="/mobx" /> + <TextFields path="/textfields" /> + <ReduxBug path="/reduxBug/:start" /> + <SuspenseRouterBug path="/suspense-router" /> + <NestedSuspenseBug path="/nested-suspense" /> + <Contenteditable path="/contenteditable" /> + </Router> + </main> + </div> + ); + } +} + +function EmptyFragment() { + return <Fragment />; +} + +// document.body.innerHTML = renderToString(<App url={location.href.match(/[#&]ssr/) ? undefined : '/'} />); +// document.body.firstChild.setAttribute('is-ssr', 'true'); + +installLogger( + String(localStorage.LOG) === 'true' || location.href.match(/logger/), + String(localStorage.CONSOLE) === 'true' || location.href.match(/console/) +); + +render(<App />, document.body); diff --git a/preact/demo/key_bug.js b/preact/demo/key_bug.js new file mode 100644 index 0000000..0ccb1a6 --- /dev/null +++ b/preact/demo/key_bug.js @@ -0,0 +1,32 @@ +import { createElement, Component } from 'preact'; + +function Foo(props) { + return <div>This is: {props.children}</div>; +} + +export default class KeyBug extends Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + this.state = { active: false }; + } + + onClick() { + this.setState(prev => ({ active: !prev.active })); + } + + render() { + return ( + <div> + {this.state.active && <Foo>foo</Foo>} + <h1>Hello World</h1> + <br /> + <Foo> + bar <Foo>bar</Foo> + </Foo> + <br /> + <button onClick={this.onClick}>Toggle</button> + </div> + ); + } +} diff --git a/preact/demo/list.js b/preact/demo/list.js new file mode 100644 index 0000000..2ef968c --- /dev/null +++ b/preact/demo/list.js @@ -0,0 +1,63 @@ +import { h, render } from 'preact'; +import htm from 'htm'; +import './style.css'; + +const html = htm.bind(h); +const createRoot = parent => ({ + render: v => render(v, parent) +}); + +function List({ items, renders, useKeys, useCounts, update }) { + const toggleKeys = () => update({ useKeys: !useKeys }); + const toggleCounts = () => update({ useCounts: !useCounts }); + const swap = () => { + const u = { items: items.slice() }; + u.items[1] = items[8]; + u.items[8] = items[1]; + update(u); + }; + return html` + <div> + <button onClick=${update}>Re-render</button> + <button onClick=${swap}>Swap 2 & 8</button> + <label> + <input type="checkbox" checked=${useKeys} onClick=${toggleKeys} /> + Use Keys + </label> + <label> + <input type="checkbox" checked=${useCounts} onClick=${toggleCounts} /> + Counts + </label> + <ul class="list"> + ${items.map( + (item, i) => html` + <li + class=${i % 2 ? 'odd' : 'even'} + key=${useKeys ? item.name : undefined} + > + ${item.name} ${useCounts ? ` (${renders} renders)` : ''} + </li> + ` + )} + </ul> + </div> + `; +} + +const root = createRoot(document.body); + +let data = { + items: new Array(1000).fill(null).map((x, i) => ({ name: `Item ${i + 1}` })), + renders: 0, + useKeys: false, + useCounts: false +}; + +function update(partial) { + if (partial) Object.assign(data, partial); + data.renders++; + data.update = update; + root.render(List(data)); +} + +update(); diff --git a/preact/demo/logger.js b/preact/demo/logger.js new file mode 100644 index 0000000..59df9b9 --- /dev/null +++ b/preact/demo/logger.js @@ -0,0 +1,170 @@ +export default function logger(logStats, logConsole) { + if (!logStats && !logConsole) { + return; + } + + const consoleBuffer = new ConsoleBuffer(); + + let calls = {}; + let lock = true; + + function serialize(obj) { + if (obj instanceof Text) return '#text'; + if (obj instanceof Element) return `<${obj.localName}>`; + if (obj === document) return 'document'; + return Object.prototype.toString.call(obj).replace(/(^\[object |\]$)/g, ''); + } + + function count(key) { + if (lock === true) return; + calls[key] = (calls[key] || 0) + 1; + + if (logConsole) { + consoleBuffer.log(key); + } + } + + function logCall(obj, method, name) { + let old = obj[method]; + obj[method] = function() { + let c = ''; + for (let i = 0; i < arguments.length; i++) { + if (c) c += ', '; + c += serialize(arguments[i]); + } + count(`${serialize(this)}.${method}(${c})`); + return old.apply(this, arguments); + }; + } + + logCall(document, 'createElement'); + logCall(document, 'createElementNS'); + logCall(Element.prototype, 'remove'); + logCall(Element.prototype, 'appendChild'); + logCall(Element.prototype, 'removeChild'); + logCall(Element.prototype, 'insertBefore'); + logCall(Element.prototype, 'replaceChild'); + logCall(Element.prototype, 'setAttribute'); + logCall(Element.prototype, 'setAttributeNS'); + logCall(Element.prototype, 'removeAttribute'); + logCall(Element.prototype, 'removeAttributeNS'); + let d = + Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data') || + Object.getOwnPropertyDescriptor(Node.prototype, 'data'); + Object.defineProperty(Text.prototype, 'data', { + get() { + let value = d.get.call(this); + count(`get #text.data`); + return value; + }, + set(v) { + count(`set #text.data`); + return d.set.call(this, v); + } + }); + + let root; + function setup() { + if (!logStats) return; + + lock = true; + root = document.createElement('table'); + root.style.cssText = + 'position: fixed; right: 0; top: 0; z-index:999; background: #000; font-size: 12px; color: #FFF; opacity: 0.9; white-space: nowrap;'; + let header = document.createElement('thead'); + header.innerHTML = + '<tr><td colspan="2">Stats <button id="clear-logs">clear</button></td></tr>'; + root.tableBody = document.createElement('tbody'); + root.appendChild(root.tableBody); + root.appendChild(header); + document.documentElement.appendChild(root); + let btn = document.getElementById('clear-logs'); + btn.addEventListener('click', () => { + for (let key in calls) { + calls[key] = 0; + } + }); + lock = false; + } + + let rows = {}; + function createRow(id) { + let row = document.createElement('tr'); + row.key = document.createElement('td'); + row.key.textContent = id; + row.appendChild(row.key); + row.value = document.createElement('td'); + row.value.textContent = ' '; + row.appendChild(row.value); + root.tableBody.appendChild(row); + return (rows[id] = row); + } + + function insertInto(parent) { + parent.appendChild(root); + } + + function remove() { + clearInterval(updateTimer); + } + + function update() { + if (!logStats) return; + + lock = true; + for (let i in calls) { + if (calls.hasOwnProperty(i)) { + let row = rows[i] || createRow(i); + row.value.firstChild.nodeValue = calls[i]; + } + } + lock = false; + } + + let updateTimer = setInterval(update, 50); + + setup(); + lock = false; + return { insertInto, update, remove }; +} + +/** + * Logging to the console significantly affects performance. + * Buffer calls to console and replay them at the end of the + * current stack + * @extends {Console} + */ +class ConsoleBuffer { + constructor() { + /** @type {Array<[string, any[]]>} */ + this.buffer = []; + this.deferred = null; + + for (let methodName of Object.keys(console)) { + this[methodName] = this.proxy(methodName); + } + } + + proxy(methodName) { + return (...args) => { + this.buffer.push([methodName, args]); + this.deferFlush(); + }; + } + + deferFlush() { + if (this.deferred == null) { + this.deferred = Promise.resolve() + .then(() => this.flush()) + .then(() => (this.deferred = null)); + } + } + + flush() { + let method; + while ((method = this.buffer.shift())) { + let [name, args] = method; + console[name](...args); + } + } +} diff --git a/preact/demo/mobx.js b/preact/demo/mobx.js new file mode 100644 index 0000000..8e4159f --- /dev/null +++ b/preact/demo/mobx.js @@ -0,0 +1,75 @@ +import React, { createElement, forwardRef, useRef, useState } from 'react'; +import { decorate, observable } from 'mobx'; +import { observer, useObserver } from 'mobx-react'; +import 'mobx-react-lite/batchingForReactDom'; + +class Todo { + constructor() { + this.id = Math.random(); + this.title = 'initial'; + this.finished = false; + } +} +decorate(Todo, { + title: observable, + finished: observable +}); + +const Forward = observer( + // eslint-disable-next-line react/display-name + forwardRef(({ todo }, ref) => { + return ( + <p ref={ref}> + Forward: "{todo.title}" {'' + todo.finished} + </p> + ); + }) +); + +const todo = new Todo(); + +const TodoView = observer(({ todo }) => { + return ( + <p> + Todo View: "{todo.title}" {'' + todo.finished} + </p> + ); +}); + +const HookView = ({ todo }) => { + return useObserver(() => { + return ( + <p> + Todo View: "{todo.title}" {'' + todo.finished} + </p> + ); + }); +}; + +export function MobXDemo() { + const ref = useRef(null); + let [v, set] = useState(0); + + const success = ref.current && ref.current.nodeName === 'P'; + + return ( + <div> + <input + type="text" + placeholder="type here..." + onInput={e => { + todo.title = e.target.value; + set(v + 1); + }} + /> + <p> + <b style={`color: ${success ? 'green' : 'red'}`}> + {success ? 'SUCCESS' : 'FAIL'} + </b> + </p> + <TodoView todo={todo} /> + <Forward todo={todo} ref={ref} /> + <HookView todo={todo} /> + </div> + ); +} diff --git a/preact/demo/nested-suspense/addnewcomponent.js b/preact/demo/nested-suspense/addnewcomponent.js new file mode 100644 index 0000000..e3e4695 --- /dev/null +++ b/preact/demo/nested-suspense/addnewcomponent.js @@ -0,0 +1,5 @@ +import { createElement } from 'react'; + +export default function AddNewComponent({ appearance }) { + return <div>AddNewComponent (component #{appearance})</div>; +} diff --git a/preact/demo/nested-suspense/component-container.js b/preact/demo/nested-suspense/component-container.js new file mode 100644 index 0000000..b1da87b --- /dev/null +++ b/preact/demo/nested-suspense/component-container.js @@ -0,0 +1,17 @@ +import { createElement, lazy } from 'react'; + +const pause = timeout => + new Promise(d => setTimeout(d, timeout), console.log(timeout)); + +const SubComponent = lazy(() => + pause(Math.random() * 1000).then(() => import('./subcomponent.js')) +); + +export default function ComponentContainer({ appearance }) { + return ( + <div> + GenerateComponents (component #{appearance}) + <SubComponent /> + </div> + ); +} diff --git a/preact/demo/nested-suspense/dropzone.js b/preact/demo/nested-suspense/dropzone.js new file mode 100644 index 0000000..c4a28b4 --- /dev/null +++ b/preact/demo/nested-suspense/dropzone.js @@ -0,0 +1,5 @@ +import { createElement } from 'react'; + +export default function DropZone({ appearance }) { + return <div>DropZone (component #{appearance})</div>; +} diff --git a/preact/demo/nested-suspense/editor.js b/preact/demo/nested-suspense/editor.js new file mode 100644 index 0000000..253d130 --- /dev/null +++ b/preact/demo/nested-suspense/editor.js @@ -0,0 +1,5 @@ +import { createElement } from 'react'; + +export default function Editor({ children }) { + return <div className="Editor">{children}</div>; +} diff --git a/preact/demo/nested-suspense/index.js b/preact/demo/nested-suspense/index.js new file mode 100644 index 0000000..6c525b3 --- /dev/null +++ b/preact/demo/nested-suspense/index.js @@ -0,0 +1,69 @@ +import { createElement, Suspense, lazy, Component } from 'react'; + +const Loading = function() { + return <div>Loading...</div>; +}; +const Error = function({ resetState }) { + return ( + <div> + Error! + <a onClick={resetState} href="#"> + Reset app + </a> + </div> + ); +}; + +const pause = timeout => + new Promise(d => setTimeout(d, timeout), console.log(timeout)); + +const DropZone = lazy(() => + pause(Math.random() * 1000).then(() => import('./dropzone.js')) +); +const Editor = lazy(() => + pause(Math.random() * 1000).then(() => import('./editor.js')) +); +const AddNewComponent = lazy(() => + pause(Math.random() * 1000).then(() => import('./addnewcomponent.js')) +); +const GenerateComponents = lazy(() => + pause(Math.random() * 1000).then(() => import('./component-container.js')) +); + +export default class App extends Component { + state = { hasError: false }; + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI. + console.warn(error); + return { hasError: true }; + } + + render() { + return this.state.hasError ? ( + <Error resetState={() => this.setState({ hasError: false })} /> + ) : ( + <Suspense fallback={<Loading />}> + <DropZone appearance={0} /> + <Editor title="APP_TITLE"> + <main> + <Suspense fallback={<Loading />}> + <GenerateComponents appearance={1} /> + </Suspense> + <AddNewComponent appearance={2} /> + </main> + <aside> + <section> + <Suspense fallback={<Loading />}> + <GenerateComponents appearance={3} /> + </Suspense> + <AddNewComponent appearance={4} /> + </section> + </aside> + </Editor> + + <footer>Footer here</footer> + </Suspense> + ); + } +} diff --git a/preact/demo/nested-suspense/subcomponent.js b/preact/demo/nested-suspense/subcomponent.js new file mode 100644 index 0000000..74d6d1d --- /dev/null +++ b/preact/demo/nested-suspense/subcomponent.js @@ -0,0 +1,5 @@ +import { createElement } from 'react'; + +export default function SubComponent({ onClick }) { + return <div>Lazy loaded sub component</div>; +} diff --git a/preact/demo/old.js.bak b/preact/demo/old.js.bak new file mode 100644 index 0000000..7f2b7c0 --- /dev/null +++ b/preact/demo/old.js.bak @@ -0,0 +1,103 @@ + +// function createRoot(title) { +// let div = document.createElement('div'); +// let h2 = document.createElement('h2'); +// h2.textContent = title; +// div.appendChild(h2); +// document.body.appendChild(div); +// return div; +// } + + +/* +function logCall(obj, method, name) { + let old = obj[method]; + obj[method] = function(...args) { + console.log(`<${this.localName}>.`+(name || `${method}(${args})`)); + return old.apply(this, args); + }; +} + +logCall(HTMLElement.prototype, 'appendChild'); +logCall(HTMLElement.prototype, 'removeChild'); +logCall(HTMLElement.prototype, 'insertBefore'); +logCall(HTMLElement.prototype, 'replaceChild'); +logCall(HTMLElement.prototype, 'setAttribute'); +logCall(HTMLElement.prototype, 'removeAttribute'); +let d = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeValue'); +Object.defineProperty(Text.prototype, 'nodeValue', { + get() { + let value = d.get.call(this); + console.log('get Text#nodeValue: ', value); + return value; + }, + set(v) { + console.log('set Text#nodeValue', v); + return d.set.call(this, v); + } +}); + + +render(( + <div> + <h4>This is a test.</h4> + <Foo /> + <time>...</time> + </div> +), createRoot('Stateful component update demo:')); + + +class Foo extends Component { + componentDidMount() { + console.log('mounted'); + this.timer = setInterval( () => { + this.setState({ time: Date.now() }); + }, 5000); + } + componentWillUnmount() { + clearInterval(this.timer); + } + render(props, state, context) { + // console.log('rendering', props, state, context); + return <time>test: {state.time}</time> + } +} + + +render(( + <div> + <h4>This is a test.</h4> + <Foo /> + <time>...</time> + </div> +), createRoot('Stateful component update demo:')); + + +let items = []; +let count = 0; +let three = createRoot('Top-level render demo:'); + +setInterval( () => { + if (count++ %20 < 10 ) { + items.push(<li key={count} style={{ + position: 'relative', + transition: 'all 200ms ease', + paddingLeft: items.length*20 +'px' + }}>item #{items.length}</li>); + } + else { + items.shift(); + } + + render(( + <div> + <h4>This is a test.</h4> + <time>{Date.now()}</time> + <ul>{items}</ul> + </div> + ), three); +}, 5000); + +// Mount the top-level component to the DOM: +render(<Main />, document.body); +*/ diff --git a/preact/demo/package.json b/preact/demo/package.json new file mode 100644 index 0000000..21cfb36 --- /dev/null +++ b/preact/demo/package.json @@ -0,0 +1,44 @@ +{ + "name": "demo", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --inline" + }, + "devDependencies": { + "@babel/core": "^7.0.0-beta.55", + "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55", + "@babel/plugin-proposal-decorators": "^7.4.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-transform-react-constant-elements": "^7.0.0-beta.55", + "@babel/plugin-transform-react-jsx": "^7.0.0-beta.55", + "@babel/plugin-transform-runtime": "^7.4.0", + "@babel/preset-env": "^7.0.0-beta.55", + "@babel/preset-react": "^7.0.0-beta.55", + "@babel/preset-typescript": "^7.3.3", + "babel-loader": "^8.0.0-beta.0", + "css-loader": "2.1.1", + "html-webpack-plugin": "3.2.0", + "node-sass": "^4.12.0", + "sass-loader": "7.1.0", + "style-loader": "0.23.1", + "webpack": "4.33.0", + "webpack-cli": "^3.3.4", + "webpack-dev-server": "^3.7.1" + }, + "dependencies": { + "@material-ui/core": "4.9.5", + "d3-scale": "^1.0.7", + "d3-selection": "^1.2.0", + "htm": "2.1.1", + "mobx": "^5.15.4", + "mobx-react": "^6.2.2", + "mobx-state-tree": "^3.16.0", + "preact-render-to-string": "^5.0.2", + "preact-router": "^3.0.0", + "react-redux": "^7.1.0", + "react-router": "^5.0.1", + "react-router-dom": "^5.0.1", + "redux": "^4.0.1", + "styled-components": "^4.2.0" + } +} diff --git a/preact/demo/people/Readme.md b/preact/demo/people/Readme.md new file mode 100644 index 0000000..c18a1cb --- /dev/null +++ b/preact/demo/people/Readme.md @@ -0,0 +1,3 @@ +# People demo page + +This section of our demo was originally made by [phaux](https://github.com/phaux) in the [web-app-boilerplate](https://github.com/phaux/web-app-boilerplate) repo. It has been slightly modified from it's original to better work inside of our demo app diff --git a/preact/demo/people/index.tsx b/preact/demo/people/index.tsx new file mode 100644 index 0000000..0e728ab --- /dev/null +++ b/preact/demo/people/index.tsx @@ -0,0 +1,59 @@ +import { observer } from 'mobx-react'; +import { Component, h } from 'preact'; +import { Profile } from './profile'; +import { Link, Route, Router } from './router'; +import { store } from './store'; + +import './styles/index.scss'; + +@observer +export default class App extends Component { + componentDidMount() { + store.loadUsers().catch(console.error); + } + + render() { + return ( + <Router> + <div id="people-app"> + <nav> + <div style={{ margin: 16, textAlign: 'center' }}> + Sort by{' '} + <select + value={store.usersOrder} + onChange={(ev: any) => { + store.setUsersOrder(ev.target.value); + }} + > + <option value="name">Name</option> + <option value="id">ID</option> + </select> + </div> + <ul> + {store.getSortedUsers().map((user, i) => ( + <li + key={user.id} + style={{ + animationDelay: `${i * 20}ms`, + top: `calc(var(--menu-item-height) * ${i})`, + transitionDelay: `${i * 20}ms` + }} + > + <Link href={`people/${user.id}`} active> + <img class="avatar" src={user.picture.large} /> + {user.name.first} {user.name.last} + </Link> + </li> + ))} + </ul> + </nav> + <section id="people-main"> + <Route match="people"> + <Route match="*" component={Profile} /> + </Route> + </section> + </div> + </Router> + ); + } +} diff --git a/preact/demo/people/profile.tsx b/preact/demo/people/profile.tsx new file mode 100644 index 0000000..e1f44ea --- /dev/null +++ b/preact/demo/people/profile.tsx @@ -0,0 +1,59 @@ +import { computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Component, h } from 'preact'; +import { RouteChildProps } from './router'; +import { store } from './store'; + +export type ProfileProps = RouteChildProps; +@observer +export class Profile extends Component<ProfileProps> { + @observable id = ''; + @observable busy = false; + + componentDidMount() { + this.id = this.props.route; + } + + componentWillReceiveProps(props: ProfileProps) { + this.id = props.route; + } + + render() { + const user = this.user; + if (user == null) return null; + return ( + <div class="profile"> + <img class="avatar" src={user.picture.large} /> + <h2> + {user.name.first} {user.name.last} + </h2> + <div class="details"> + <p> + {user.gender === 'female' ? '👩' : '👨'} {user.id} + </p> + <p>🖂 {user.email}</p> + </div> + <p> + <button + class={this.busy ? 'secondary busy' : 'secondary'} + disabled={this.busy} + onClick={this.remove} + > + Remove contact + </button> + </p> + </div> + ); + } + + @computed get user() { + return store.users.find(u => u.id === this.id); + } + + remove = async () => { + this.busy = true; + await new Promise<void>(cb => setTimeout(cb, 1500)); + store.deleteUser(this.id); + this.busy = false; + }; +} diff --git a/preact/demo/people/router.tsx b/preact/demo/people/router.tsx new file mode 100644 index 0000000..56fc420 --- /dev/null +++ b/preact/demo/people/router.tsx @@ -0,0 +1,153 @@ +import { + ComponentChild, + ComponentFactory, + createContext, + FunctionalComponent, + h, + JSX +} from 'preact'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useState +} from 'preact/hooks'; + +export type RouterData = { + match: string[]; + path: string[]; + navigate(path: string): void; +}; + +const RouterContext = createContext<RouterData>({ + match: [], + path: [], + navigate() {} +}); +export const useRouter = () => useContext(RouterContext); + +const useLocation = (cb: () => void) => { + useEffect(() => { + window.addEventListener('popstate', cb); + return () => { + window.removeEventListener('popstate', cb); + }; + }, [cb]); +}; + +export const Router: FunctionalComponent = props => { + const [path, setPath] = useState(location.pathname); + + const update = useCallback(() => { + setPath(location.pathname); + }, [setPath]); + + useLocation(update); + + const navigate = useCallback( + (path: string) => { + history.pushState(null, '', path); + update(); + }, + [update] + ); + + const router = useMemo<RouterData>( + () => ({ + match: [], + navigate, + path: path.split('/').filter(Boolean) + }), + [navigate, path] + ); + + return <RouterContext.Provider children={props.children} value={router} />; +}; + +export type RouteChildProps = { route: string }; +export type RouteProps = { + component?: ComponentFactory<RouteChildProps>; + match: string; + render?(route: string): ComponentChild; +}; +export const Route: FunctionalComponent<RouteProps> = props => { + const router = useRouter(); + const [dir, ...subpath] = router.path; + + if (dir == null) return null; + if (props.match !== '*' && dir !== props.match) return null; + + const children = useMemo(() => { + if (props.component) return <props.component key={dir} route={dir} />; + if (props.render) return props.render(dir); + return props.children; + }, [props.component, props.render, props.children, dir]); + + const innerRouter = useMemo<RouterData>( + () => ({ + ...router, + match: [...router.match, dir], + path: subpath + }), + [router.match, dir, subpath.join('/')] + ); + + return <RouterContext.Provider children={children} value={innerRouter} />; +}; + +export type LinkProps = JSX.HTMLAttributes & { + active?: boolean | string; +}; +export const Link: FunctionalComponent<LinkProps> = props => { + const router = useRouter(); + + const classProps = [props.class, props.className]; + const originalClasses = useMemo(() => { + const classes = []; + for (const prop of classProps) if (prop) classes.push(...prop.split(/\s+/)); + return classes; + }, classProps); + + const activeClass = useMemo(() => { + if (!props.active || props.href == null) return undefined; + const href = props.href.split('/').filter(Boolean); + const path = + props.href[0] === '/' ? [...router.match, ...router.path] : router.path; + const isMatch = href.every((dir, i) => dir === path[i]); + if (isMatch) return props.active === true ? 'active' : props.active; + }, [originalClasses, props.active, props.href, router.match, router.path]); + + const classes = + activeClass == null ? originalClasses : [...originalClasses, activeClass]; + + const getHref = useCallback(() => { + if (props.href == null || props.href[0] === '/') return props.href; + const path = props.href.split('/').filter(Boolean); + return '/' + [...router.match, ...path].join('/'); + }, [router.match, props.href]); + + const handleClick = useCallback( + (ev: MouseEvent) => { + const href = getHref(); + if (props.onClick != null) props.onClick(ev); + if (ev.defaultPrevented) return; + if (href == null) return; + if (ev.button !== 0) return; + if (props.target != null && props.target !== '_self') return; + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) return; + ev.preventDefault(); + router.navigate(href); + }, + [getHref, router.navigate, props.onClick, props.target] + ); + + return ( + <a + {...props} + class={classes.join(' ')} + href={getHref()} + onClick={handleClick} + /> + ); +}; diff --git a/preact/demo/people/store.ts b/preact/demo/people/store.ts new file mode 100644 index 0000000..1967afd --- /dev/null +++ b/preact/demo/people/store.ts @@ -0,0 +1,83 @@ +import { flow, Instance, types } from 'mobx-state-tree'; + +const cmp = <T, U>(fn: (x: T) => U) => (a: T, b: T): number => + fn(a) > fn(b) ? 1 : -1; + +const User = types.model({ + email: types.string, + gender: types.enumeration(['male', 'female']), + id: types.identifier, + name: types.model({ + first: types.string, + last: types.string + }), + picture: types.model({ + large: types.string + }) +}); + +const Store = types + .model({ + users: types.array(User), + usersOrder: types.enumeration(['name', 'id']) + }) + .views(self => ({ + getSortedUsers() { + if (self.usersOrder === 'name') + return self.users.slice().sort(cmp(x => x.name.first)); + if (self.usersOrder === 'id') + return self.users.slice().sort(cmp(x => x.id)); + throw Error(`Unknown ordering ${self.usersOrder}`); + } + })) + .actions(self => ({ + addUser: flow<unknown, []>(function*() { + const data = yield fetch('https://randomuser.me/api?results=1') + .then(res => res.json()) + .then(data => + data.results.map((user: any) => ({ + ...user, + id: user.login.username + })) + ); + self.users.push(...data); + }), + loadUsers: flow<unknown, []>(function*() { + const data = yield fetch( + `https://randomuser.me/api?seed=${12321}&results=12` + ) + .then(res => res.json()) + .then(data => + data.results.map((user: any) => ({ + ...user, + id: user.login.username + })) + ); + self.users.replace(data); + }), + deleteUser(id: string) { + const user = self.users.find(u => u.id === id); + if (user != null) self.users.remove(user); + }, + setUsersOrder(order: 'name' | 'id') { + self.usersOrder = order; + } + })); + +export type StoreType = Instance<typeof Store>; +export const store = Store.create({ + usersOrder: 'name', + users: [] +}); + +// const { Provider, Consumer } = createContext<StoreType>(undefined as any) + +// export const StoreProvider: FunctionalComponent = props => { +// const store = Store.create({}) +// return <Provider value={store} children={props.children} /> +// } + +// export type StoreProps = {store: StoreType} +// export function injectStore<T>(Child: AnyComponent<T & StoreProps>): FunctionalComponent<T> { +// return props => <Consumer render={store => <Child {...props} store={store}/>}/> +// } diff --git a/preact/demo/people/styles/animations.scss b/preact/demo/people/styles/animations.scss new file mode 100644 index 0000000..c7ce8cb --- /dev/null +++ b/preact/demo/people/styles/animations.scss @@ -0,0 +1,34 @@ +@keyframes popup { + from { + box-shadow: 0 0 0 black; + opacity: 0; + transform: scale(0.9); + } + to { + box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5); + opacity: 1; + transform: none; + } +} + +@keyframes zoom { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes appear-from-left { + from { + opacity: 0; + transform: translateX(-25px); + } + to { + opacity: 1; + transform: none; + } +} diff --git a/preact/demo/people/styles/app.scss b/preact/demo/people/styles/app.scss new file mode 100644 index 0000000..4473f9e --- /dev/null +++ b/preact/demo/people/styles/app.scss @@ -0,0 +1,100 @@ +#people-app { + position: relative; + overflow: hidden; + min-height: 100vh; + animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards; + background: var(--app-background); + --menu-width: 260px; + --menu-item-height: 50px; + + @media (min-width: 1280px) { + max-width: 1280px; + min-height: calc(100vh - 64px); + margin: 32px auto; + border-radius: 10px; + } + + > nav { + position: absolute; + display: flow-root; + width: var(--menu-width); + height: 100%; + background-color: var(--app-background-secondary); + overflow-x: hidden; + overflow-y: auto; + } + + > nav h4 { + padding-left: 16px; + font-weight: normal; + text-transform: uppercase; + } + + > nav ul { + position: relative; + } + + > nav li { + position: absolute; + width: 100%; + animation: zoom 200ms forwards; + opacity: 0; + transition: top 200ms; + } + + > nav li > a { + position: relative; + display: flex; + overflow: hidden; + flex-flow: row; + align-items: center; + margin-left: 16px; + border-right: 2px solid transparent; + border-bottom-left-radius: 48px; + border-top-left-radius: 48px; + text-transform: capitalize; + transition: border 500ms; + } + + > nav li > a:hover { + background-color: var(--app-highlight); + } + + > nav li > a::after { + position: absolute; + top: 0; + right: -2px; + bottom: 0; + left: 0; + background-image: radial-gradient( + circle, + var(--app-ripple) 1%, + transparent 1% + ); + background-position: center; + background-repeat: no-repeat; + background-size: 10000%; + content: ''; + opacity: 0; + transition: opacity 700ms, background 300ms; + } + + > nav li > a:active::after { + background-size: 100%; + opacity: 0.5; + transition: none; + } + + > nav li > a.active { + border-color: var(--app-primary); + background-color: var(--app-highlight); + } + + > nav li > a > * { + margin: 8px; + } + + #people-main { + padding-left: var(--menu-width); + } +} diff --git a/preact/demo/people/styles/avatar.scss b/preact/demo/people/styles/avatar.scss new file mode 100644 index 0000000..437c6c4 --- /dev/null +++ b/preact/demo/people/styles/avatar.scss @@ -0,0 +1,16 @@ +#people-app { + .avatar { + display: inline-block; + overflow: hidden; + width: var(--avatar-size, 32px); + height: var(--avatar-size, 32px); + background-color: var(--avatar-color, var(--app-primary)); + border-radius: 50%; + font-size: calc(var(--avatar-size, 32px) * 0.5); + line-height: var(--avatar-size, 32px); + object-fit: cover; + text-align: center; + text-transform: uppercase; + white-space: nowrap; + } +} diff --git a/preact/demo/people/styles/button.scss b/preact/demo/people/styles/button.scss new file mode 100644 index 0000000..ce43138 --- /dev/null +++ b/preact/demo/people/styles/button.scss @@ -0,0 +1,115 @@ +#people-app { + button { + position: relative; + overflow: hidden; + min-width: 36px; + height: 36px; + padding: 0 16px; + border: none; + background-color: transparent; + border-radius: 4px; + color: var(--app-text); + font-family: 'Montserrat', sans-serif; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + transition: background 300ms, color 200ms; + white-space: nowrap; + } + + button::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: var(--app-ripple); + content: ''; + opacity: 0; + transition: opacity 200ms; + } + + button:hover:not(:disabled)::before { + opacity: 0.3; + transition: opacity 100ms; + } + + button:active:not(:disabled)::before { + opacity: 0.7; + transition: none; + } + + button::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-image: radial-gradient( + circle, + var(--app-ripple) 1%, + transparent 1% + ); + background-position: center; + background-repeat: no-repeat; + background-size: 20000%; + content: ''; + opacity: 0; + transition: opacity 700ms, background 400ms; + } + + button:active:not(:disabled)::after { + background-size: 100%; + opacity: 1; + transition: none; + } + + button.primary { + background-color: var(--app-primary); + box-shadow: 0 2px 6px var(--app-shadow); + } + + button.secondary { + background-color: var(--app-secondary); + box-shadow: 0 2px 6px var(--app-shadow); + } + + button:disabled { + color: var(--app-text-secondary); + } + + button.busy { + animation: stripes 500ms linear infinite; + background-image: repeating-linear-gradient( + 45deg, + var(--app-shadow) 0%, + var(--app-shadow) 25%, + transparent 25%, + transparent 50%, + var(--app-shadow) 50%, + var(--app-shadow) 75%, + transparent 75%, + transparent 100% + ); + color: var(--app-text); + /* letter-spacing: -.7em; */ + } + + button:disabled:not(.primary):not(.secondary).busy, + button:disabled.primary:not(.busy), + button:disabled.secondary:not(.busy) { + background-color: var(--app-background-disabled); + } + + @keyframes stripes { + from { + background-position-x: 0; + background-size: 16px 16px; + } + to { + background-position-x: 16px; + background-size: 16px 16px; + } + } +} diff --git a/preact/demo/people/styles/index.scss b/preact/demo/people/styles/index.scss new file mode 100644 index 0000000..80b342e --- /dev/null +++ b/preact/demo/people/styles/index.scss @@ -0,0 +1,168 @@ +@import 'app.scss'; +@import 'animations.scss'; +@import 'avatar.scss'; +@import 'profile.scss'; +@import 'button.scss'; + +// :root { +#people-app { + --app-background: #2f2b43; + --app-background-secondary: #353249; + --app-background-disabled: #555366; + --app-highlight: rgba(255, 255, 255, 0.1); + --app-ripple: rgba(255, 255, 255, 0.5); + --app-shadow: rgba(0, 0, 0, 0.15); + --app-text: #fff; + --app-text-secondary: #807e97; + --app-primary: #ff0087; + --app-secondary: #4d7cfe; + --app-tertiary: #00ec97; + --app-danger: #f3c835; + --spinner-size: 200px; +} + +* { + box-sizing: border-box; +} + +// body { +#people-app { + // display: flow-root; + // overflow: auto; + // min-height: 100vh; + // margin: 0; + // animation: background-light 5s ease-out forwards; + // /* very fancy background */ + // background: radial-gradient( + // circle 15px at 150px 90vh, + // rgba(255, 255, 255, 0.35), + // rgba(255, 255, 255, 0.35) 90%, + // transparent + // ), + // radial-gradient( + // circle 9px at 60px 50vh, + // rgba(255, 255, 255, 0.55), + // rgba(255, 255, 255, 0.55) 90%, + // transparent + // ), + // radial-gradient( + // circle 19px at 40vw 70px, + // rgba(255, 255, 255, 0.3), + // rgba(255, 255, 255, 0.3) 90%, + // transparent + // ), + // radial-gradient( + // circle 12px at 80vw 80px, + // rgba(255, 255, 255, 0.4), + // rgba(255, 255, 255, 0.4) 90%, + // transparent + // ), + // radial-gradient( + // circle 7px at 55vw calc(100vh - 95px), + // rgba(255, 255, 255, 0.6), + // rgba(255, 255, 255, 0.6) 90%, + // transparent + // ), + // radial-gradient( + // circle 14px at 25vw calc(100vh - 35px), + // rgba(255, 255, 255, 0.4), + // rgba(255, 255, 255, 0.4) 90%, + // transparent + // ), + // radial-gradient( + // circle 11px at calc(100vw - 95px) 55vh, + // rgba(255, 255, 255, 0.45), + // rgba(255, 255, 255, 0.45) 90%, + // transparent + // ), + // radial-gradient( + // circle 13px at calc(100vw - 35px) 85vh, + // rgba(255, 255, 255, 0.4), + // rgba(255, 255, 255, 0.4) 90%, + // transparent + // ), + // radial-gradient( + // circle 50vw at 0 -25%, + // rgba(255, 255, 255, 0.07), + // rgba(255, 255, 255, 0.07) 100%, + // transparent + // ), + // radial-gradient( + // circle 80vw at top left, + // rgba(255, 255, 255, 0.07), + // rgba(255, 255, 255, 0.07) 100%, + // transparent + // ), + // radial-gradient(circle at bottom right, #ef2fb8, transparent), + // radial-gradient(circle at top right, #c45af3, transparent), + // linear-gradient(#ee66ca, #ff47a6); + color: var(--app-text); + font-family: 'Montserrat', sans-serif; +} + +#people-app { + .spinner { + position: absolute; + top: 200px; + left: calc(50% - var(--spinner-size) / 2); + width: var(--spinner-size); + height: var(--spinner-size); + animation: zoom 250ms 500ms forwards ease-out; + opacity: 0; + transition: opacity 200ms, transform 200ms ease-in; + } + + .spinner.exit { + opacity: 0; + transform: scale(0.5); + } + + .spinner::before, + .spinner::after { + position: absolute; + top: 0; + left: 0; + width: calc(var(--spinner-size) / 3); + height: calc(var(--spinner-size) / 3); + animation: spinner 2s infinite ease-in-out; + background-color: rgba(255, 255, 255, 0.6); + content: ''; + } + + .spinner::after { + animation-delay: -1s; + } + + @keyframes spinner { + 25% { + transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px)) + rotate(-90deg) scale(0.5); + } + 50% { + transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px)) + translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-179deg); + } + 50.1% { + transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px)) + translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-180deg); + } + 75% { + transform: translateX(0) translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) + rotate(-270deg) scale(0.5); + } + 100% { + transform: rotate(-360deg); + } + } + + ul, + ol { + padding-left: 0; + list-style: none; + } + + a { + color: inherit; + text-decoration: none; + } +} diff --git a/preact/demo/people/styles/profile.scss b/preact/demo/people/styles/profile.scss new file mode 100644 index 0000000..f74b760 --- /dev/null +++ b/preact/demo/people/styles/profile.scss @@ -0,0 +1,26 @@ +#people-app { + .profile { + display: flex; + flex-flow: column; + align-items: center; + margin: 32px 0; + animation: appear-from-left 0.5s forwards; + --avatar-size: 80px; + } + + .profile h2 { + text-transform: capitalize; + } + + .profile .details { + display: flex; + flex-flow: column; + align-items: stretch; + margin: 16px auto; + } + + .profile .details p { + margin-top: 8px; + margin-bottom: 8px; + } +} diff --git a/preact/demo/preact.js b/preact/demo/preact.js new file mode 100644 index 0000000..9b053e0 --- /dev/null +++ b/preact/demo/preact.js @@ -0,0 +1,39 @@ +import { + options, + createElement, + cloneElement, + Component as CevicheComponent, + render +} from 'preact'; + +options.vnode = vnode => { + vnode.nodeName = vnode.type; + vnode.attributes = vnode.props; + vnode.children = vnode._children || [].concat(vnode.props.children || []); +}; + +function asArray(arr) { + return Array.isArray(arr) ? arr : [arr]; +} + +function normalize(obj) { + if (Array.isArray(obj)) { + return obj.map(normalize); + } + if ('type' in obj && !('attributes' in obj)) { + obj.attributes = obj.props; + } + return obj; +} + +export function Component(props, context) { + CevicheComponent.call(this, props, context); + const render = this.render; + this.render = function(props, state, context) { + if (props.children) props.children = asArray(normalize(props.children)); + return render.call(this, props, state, context); + }; +} +Component.prototype = new CevicheComponent(); + +export { createElement, createElement as h, cloneElement, render }; diff --git a/preact/demo/profiler.js b/preact/demo/profiler.js new file mode 100644 index 0000000..bb44269 --- /dev/null +++ b/preact/demo/profiler.js @@ -0,0 +1,88 @@ +import { createElement, Component, options } from 'preact'; + +function getPrimes(max) { + let sieve = [], + i, + j, + primes = []; + for (i = 2; i <= max; ++i) { + if (!sieve[i]) { + // i has not been marked -- it is prime + primes.push(i); + for (j = i << 1; j <= max; j += i) { + sieve[j] = true; + } + } + } + return primes.join(''); +} + +function Foo(props) { + return <div>{props.children}</div>; +} + +function Bar() { + getPrimes(10000); + return ( + <div> + <span>...yet another component</span> + </div> + ); +} + +function PrimeNumber(props) { + // Slow down rendering of this component + getPrimes(10); + + return ( + <div> + <span>I'm a slow component</span> + <br /> + {props.children} + </div> + ); +} + +export default class ProfilerDemo extends Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + this.state = { counter: 0 }; + } + + componentDidMount() { + options._diff = vnode => (vnode.startTime = performance.now()); + options.diffed = vnode => (vnode.endTime = performance.now()); + } + + componentWillUnmount() { + delete options._diff; + delete options.diffed; + } + + onClick() { + this.setState(prev => ({ counter: ++prev.counter })); + } + + render() { + return ( + <div class="foo"> + <h1>⚛ Preact</h1> + <p> + <b>Devtools Profiler integration 🕒</b> + </p> + <Foo> + <PrimeNumber> + <Foo>I'm a fast component</Foo> + <Bar /> + </PrimeNumber> + </Foo> + <Foo>I'm the fastest component 🎉</Foo> + <span>Counter: {this.state.counter}</span> + <br /> + <br /> + <button onClick={this.onClick}>Force re-render</button> + </div> + ); + } +} diff --git a/preact/demo/pythagoras/index.js b/preact/demo/pythagoras/index.js new file mode 100644 index 0000000..470e304 --- /dev/null +++ b/preact/demo/pythagoras/index.js @@ -0,0 +1,86 @@ +import { createElement, Component } from 'preact'; +import { select as d3select, mouse as d3mouse } from 'd3-selection'; +import { scaleLinear } from 'd3-scale'; +import Pythagoras from './pythagoras'; + +export default class PythagorasDemo extends Component { + svg = { + width: 1280, + height: 600 + }; + + state = { + currentMax: 0, + baseW: 80, + heightFactor: 0, + lean: 0 + }; + + realMax = 11; + + svgRef = c => { + this.svgElement = c; + }; + + scaleFactor = scaleLinear() + .domain([this.svg.height, 0]) + .range([0, 0.8]); + + scaleLean = scaleLinear() + .domain([0, this.svg.width / 2, this.svg.width]) + .range([0.5, 0, -0.5]); + + onMouseMove = event => { + let [x, y] = d3mouse(this.svgElement); + + this.setState({ + heightFactor: this.scaleFactor(y), + lean: this.scaleLean(x) + }); + }; + + restart = () => { + this.setState({ currentMax: 0 }); + this.next(); + }; + + next = () => { + let { currentMax } = this.state; + + if (currentMax < this.realMax) { + this.setState({ currentMax: currentMax + 1 }); + this.timer = setTimeout(this.next, 500); + } + }; + + componentDidMount() { + this.selected = d3select(this.svgElement).on('mousemove', this.onMouseMove); + this.next(); + } + + componentWillUnmount() { + this.selected.on('mousemove', null); + clearTimeout(this.timer); + } + + render({}, { currentMax, baseW, heightFactor, lean }) { + let { width, height } = this.svg; + + return ( + <div class="App"> + <svg width={width} height={height} ref={this.svgRef}> + <Pythagoras + w={baseW} + h={baseW} + heightFactor={heightFactor} + lean={lean} + x={width / 2 - 40} + y={height - baseW} + lvl={0} + maxlvl={currentMax} + /> + </svg> + </div> + ); + } +} diff --git a/preact/demo/pythagoras/pythagoras.js b/preact/demo/pythagoras/pythagoras.js new file mode 100644 index 0000000..bbe3c65 --- /dev/null +++ b/preact/demo/pythagoras/pythagoras.js @@ -0,0 +1,97 @@ +import { interpolateViridis } from 'd3-scale'; +import { createElement } from 'preact'; + +Math.deg = function(radians) { + return radians * (180 / Math.PI); +}; + +const memoizedCalc = (function() { + const memo = {}; + + const key = ({ w, heightFactor, lean }) => `${w}-${heightFactor}-${lean}`; + + return args => { + let memoKey = key(args); + + if (memo[memoKey]) { + return memo[memoKey]; + } + + let { w, heightFactor, lean } = args; + let trigH = heightFactor * w; + + let result = { + nextRight: Math.sqrt(trigH ** 2 + (w * (0.5 + lean)) ** 2), + nextLeft: Math.sqrt(trigH ** 2 + (w * (0.5 - lean)) ** 2), + A: Math.deg(Math.atan(trigH / ((0.5 - lean) * w))), + B: Math.deg(Math.atan(trigH / ((0.5 + lean) * w))) + }; + + memo[memoKey] = result; + return result; + }; +})(); + +export default function Pythagoras({ + w, + x, + y, + heightFactor, + lean, + left, + right, + lvl, + maxlvl +}) { + if (lvl >= maxlvl || w < 1) { + return null; + } + + const { nextRight, nextLeft, A, B } = memoizedCalc({ + w, + heightFactor, + lean + }); + + let rotate = ''; + + if (left) { + rotate = `rotate(${-A} 0 ${w})`; + } else if (right) { + rotate = `rotate(${B} ${w} ${w})`; + } + + return ( + <g transform={`translate(${x} ${y}) ${rotate}`}> + <rect + width={w} + height={w} + x={0} + y={0} + style={{ fill: interpolateViridis(lvl / maxlvl) }} + /> + + <Pythagoras + w={nextLeft} + x={0} + y={-nextLeft} + lvl={lvl + 1} + maxlvl={maxlvl} + heightFactor={heightFactor} + lean={lean} + left + /> + + <Pythagoras + w={nextRight} + x={w - nextRight} + y={-nextRight} + lvl={lvl + 1} + maxlvl={maxlvl} + heightFactor={heightFactor} + lean={lean} + right + /> + </g> + ); +} diff --git a/preact/demo/redux.js b/preact/demo/redux.js new file mode 100644 index 0000000..d63dfb7 --- /dev/null +++ b/preact/demo/redux.js @@ -0,0 +1,48 @@ +import { createElement } from 'preact'; +import React from 'react'; +import { createStore } from 'redux'; +import { connect, Provider } from 'react-redux'; + +const store = createStore((state = { value: 0 }, action) => { + switch (action.type) { + case 'increment': + return { value: state.value + 1 }; + case 'decrement': + return { value: state.value - 1 }; + default: + return state; + } +}); + +class Child extends React.Component { + render() { + return ( + <div> + <div>Child #1: {this.props.foo}</div> + <ConnectedChild2 /> + </div> + ); + } +} +const ConnectedChild = connect(store => ({ foo: store.value }))(Child); + +class Child2 extends React.Component { + render() { + return <div>Child #2: {this.props.foo}</div>; + } +} +const ConnectedChild2 = connect(store => ({ foo: store.value }))(Child2); + +export default function Redux() { + return ( + <div> + <h1>Counter</h1> + <Provider store={store}> + <ConnectedChild /> + </Provider> + <br /> + <button onClick={() => store.dispatch({ type: 'increment' })}>+</button> + <button onClick={() => store.dispatch({ type: 'decrement' })}>-</button> + </div> + ); +} diff --git a/preact/demo/reduxUpdate.js b/preact/demo/reduxUpdate.js new file mode 100644 index 0000000..37e1f9c --- /dev/null +++ b/preact/demo/reduxUpdate.js @@ -0,0 +1,61 @@ +import { createElement, Component } from 'preact'; +import { connect, Provider } from 'react-redux'; +import { createStore } from 'redux'; +import { HashRouter, Route, Link } from 'react-router-dom'; + +const store = createStore( + (state, action) => ({ ...state, display: action.display }), + { display: false } +); + +function _Redux({ showMe, counter }) { + if (!showMe) return null; + return <div>showMe {counter}</div>; +} +const Redux = connect( + state => console.log('injecting', state.display) || { showMe: state.display } +)(_Redux); + +let display = false; +class Test extends Component { + componentDidUpdate(prevProps) { + if (this.props.start != prevProps.start) { + this.setState({ f: (this.props.start || 0) + 1 }); + setTimeout(() => this.setState({ i: (this.state.i || 0) + 1 })); + } + } + + render() { + const { f } = this.state; + return ( + <div> + <button + onClick={() => { + display = !display; + store.dispatch({ type: 1, display }); + }} + > + Toggle visibility + </button> + <Link to={`/${(parseInt(this.props.start) || 0) + 1}`}>Click me</Link> + + <Redux counter={f} /> + </div> + ); + } +} + +function App() { + return ( + <Provider store={store}> + <HashRouter> + <Route + path="/:start?" + render={({ match }) => <Test start={match.params.start || 0} />} + /> + </HashRouter> + </Provider> + ); +} + +export default App; diff --git a/preact/demo/reorder.js b/preact/demo/reorder.js new file mode 100644 index 0000000..35acc5e --- /dev/null +++ b/preact/demo/reorder.js @@ -0,0 +1,104 @@ +import { createElement, Component } from 'preact'; + +function createItems(count = 10) { + let items = []; + for (let i = 0; i < count; i++) { + items.push({ + label: `Item #${i + 1}`, + key: i + 1 + }); + } + return items; +} + +function random() { + return Math.random() < 0.5 ? 1 : -1; +} + +export default class Reorder extends Component { + state = { + items: createItems(), + count: 1, + useKeys: false + }; + + shuffle = () => { + this.setState({ items: this.state.items.slice().sort(random) }); + }; + + swapTwo = () => { + let items = this.state.items.slice(), + first = Math.floor(Math.random() * items.length), + second; + do { + second = Math.floor(Math.random() * items.length); + } while (second === first); + let other = items[first]; + items[first] = items[second]; + items[second] = other; + this.setState({ items }); + }; + + reverse = () => { + this.setState({ items: this.state.items.slice().reverse() }); + }; + + setCount = e => { + this.setState({ count: Math.round(e.target.value) }); + }; + + rotate = () => { + let { items, count } = this.state; + items = items.slice(count).concat(items.slice(0, count)); + this.setState({ items }); + }; + + rotateBackward = () => { + let { items, count } = this.state, + len = items.length; + items = items.slice(len - count, len).concat(items.slice(0, len - count)); + this.setState({ items }); + }; + + toggleKeys = () => { + this.setState({ useKeys: !this.state.useKeys }); + }; + + renderItem = item => ( + <li key={this.state.useKeys ? item.key : null}>{item.label}</li> + ); + + render({}, { items, count, useKeys }) { + return ( + <div class="reorder-demo"> + <header> + <button onClick={this.shuffle}>Shuffle</button> + <button onClick={this.swapTwo}>Swap Two</button> + <button onClick={this.reverse}>Reverse</button> + <button onClick={this.rotate}>Rotate</button> + <button onClick={this.rotateBackward}>Rotate Backward</button> + <label> + <input + type="checkbox" + onClick={this.toggleKeys} + checked={useKeys} + />{' '} + use keys? + </label> + <label> + <input + type="number" + step="1" + min="1" + style={{ width: '3em' }} + onInput={this.setCount} + value={count} + />{' '} + count + </label> + </header> + <ul>{items.map(this.renderItem)}</ul> + </div> + ); + } +} diff --git a/preact/demo/spiral.js b/preact/demo/spiral.js new file mode 100644 index 0000000..baaa052 --- /dev/null +++ b/preact/demo/spiral.js @@ -0,0 +1,140 @@ +import { createElement, Component } from 'preact'; + +const COUNT = 500; +const LOOPS = 6; + +// Component.debounce = requestAnimationFrame; + +export default class Spiral extends Component { + state = { x: 0, y: 0, big: false, counter: 0 }; + + handleClick = e => { + console.log('click'); + }; + + increment = () => { + if (this.stop) return; + // this.setState({ counter: this.state.counter + 1 }, this.increment); + this.setState({ counter: this.state.counter + 1 }); + // this.forceUpdate(); + requestAnimationFrame(this.increment); + }; + + setMouse({ pageX: x, pageY: y }) { + this.setState({ x, y }); + return false; + } + + setBig(big) { + this.setState({ big }); + } + + componentDidMount() { + console.log('mount'); + + // let touch = navigator.maxTouchPoints > 1; + let touch = false; + + // set mouse position state on move: + addEventListener(touch ? 'touchmove' : 'mousemove', e => { + this.setMouse(e.touches ? e.touches[0] : e); + }); + + // holding the mouse down enables big mode: + addEventListener(touch ? 'touchstart' : 'mousedown', e => { + this.setBig(true); + e.preventDefault(); + }); + addEventListener(touch ? 'touchend' : 'mouseup', e => this.setBig(false)); + + requestAnimationFrame(this.increment); + } + + componentWillUnmount() { + console.log('unmount'); + this.stop = true; + } + + // componentDidUpdate() { + // // invoking setState() in componentDidUpdate() creates an animation loop: + // this.increment(); + // } + + // builds and returns a brand new DOM (every time) + render(props, { x, y, big, counter }) { + let max = + COUNT + + Math.round(Math.sin((counter / 90) * 2 * Math.PI) * COUNT * 0.5), + cursors = []; + + // the advantage of JSX is that you can use the entirety of JS to "template": + for (let i = max; i--; ) { + let f = (i / max) * LOOPS, + θ = f * 2 * Math.PI, + m = 20 + i * 2, + hue = (f * 255 + counter * 10) % 255; + cursors[i] = ( + <Cursor + big={big} + color={'hsl(' + hue + ',100%,50%)'} + x={(x + Math.sin(θ) * m) | 0} + y={(y + Math.cos(θ) * m) | 0} + /> + ); + } + + return ( + <div id="main" onClick={this.handleClick}> + <Cursor label x={x} y={y} big={big} /> + {cursors} + </div> + ); + } +} + +/** Represents a single coloured dot. */ +class Cursor extends Component { + // get shared/pooled class object + getClass(big, label) { + let cl = 'cursor'; + if (big) cl += ' big'; + if (label) cl += ' label'; + return cl; + } + + // skip any pointless re-renders + shouldComponentUpdate(props) { + for (let i in props) + if (i !== 'children' && props[i] !== this.props[i]) return true; + return false; + } + + // first argument is "props", the attributes passed to <Cursor ...> + render({ x, y, label, color, big }) { + let inner = null; + if (label) + inner = ( + <span class="label"> + {x},{y} + </span> + ); + return ( + <div + class={this.getClass(big, label)} + style={{ + transform: `translate(${x || 0}px, ${y || 0}px) scale(${ + big ? 2 : 1 + })`, + // transform: `translate3d(${x || 0}px, ${y || 0}px, 0) scale(${big?2:1})`, + borderColor: color + }} + // style={{ left: x || 0, top: y || 0, borderColor: color }} + > + {inner} + </div> + ); + } +} + +// Addendum: disable dragging on mobile +addEventListener('touchstart', e => (e.preventDefault(), false)); diff --git a/preact/demo/stateOrderBug.js b/preact/demo/stateOrderBug.js new file mode 100644 index 0000000..b9f333d --- /dev/null +++ b/preact/demo/stateOrderBug.js @@ -0,0 +1,80 @@ +import htm from 'htm'; +import { h } from 'preact'; +import { useState, useCallback } from 'preact/hooks'; + +const html = htm.bind(h); + +// configuration used to show behavior vs. workaround +let childFirst = true; +const Config = () => html` + <label> + <input + type="checkbox" + checked=${childFirst} + onchange=${evt => { + childFirst = evt.target.checked; + }} + /> + Set child state before parent state. + </label> +`; + +const Child = ({ items, setItems }) => { + let [pendingId, setPendingId] = useState(null); + if (!pendingId) { + setPendingId( + (pendingId = Math.random() + .toFixed(20) + .slice(2)) + ); + } + + const onInput = useCallback( + evt => { + let val = evt.target.value, + _items = [...items, { _id: pendingId, val }]; + if (childFirst) { + setPendingId(null); + setItems(_items); + } else { + setItems(_items); + setPendingId(null); + } + }, + [childFirst, setPendingId, setItems, items, pendingId] + ); + + return html` + <div class="item-editor"> + ${items.map( + (item, idx) => html` + <input + key=${item._id} + value=${item.val} + oninput=${evt => { + let val = evt.target.value, + _items = [...items]; + _items.splice(idx, 1, { ...item, val }); + setItems(_items); + }} + /> + ` + )} + + <input + key=${pendingId} + placeholder="type to add an item" + oninput=${onInput} + /> + </div> + `; +}; + +const Parent = () => { + let [items, setItems] = useState([]); + return html` + <div><${Config} /><${Child} items=${items} setItems=${setItems} /></div> + `; +}; + +export default Parent; diff --git a/preact/demo/style.css b/preact/demo/style.css new file mode 100644 index 0000000..52f0b34 --- /dev/null +++ b/preact/demo/style.css @@ -0,0 +1,24 @@ +html, body { + font: 14px system-ui, sans-serif; +} +.list { + list-style: none; + padding: 0; +} +.list > li { + position: relative; + padding: 5px 10px; + animation: fadeIn 1s ease; +} +@keyframes fadeIn { + 0% { + box-shadow: inset 0 0 2px 2px red, + 0 0 2px 2px red; + } +} +.list > .odd { + background-color: #def; +} +.list > .even { + background-color: #fed; +} diff --git a/preact/demo/style.scss b/preact/demo/style.scss new file mode 100644 index 0000000..8466bbd --- /dev/null +++ b/preact/demo/style.scss @@ -0,0 +1,149 @@ +html, body { + height: 100%; + margin: 0; + background: #eee; + font: 400 16px/1.3 'Helvetica Neue',helvetica,sans-serif; + text-rendering: optimizeSpeed; + color: #444; +} + +.app { + display: block; + flex-direction: column; + height: 100%; + + > header { + flex: 0; + background: #f9f9f9; + box-shadow: inset 0 -.5px 0 0 rgba(0,0,0,0.2), 0 .5px 0 0 rgba(255,255,255,0.6); + + nav { + display: inline-block; + padding: 4px 7px; + + a { + display: inline-block; + margin: 2px; + padding: 4px 10px; + background-color: rgba(255,255,255,0); + border-radius: 1em; + color: #6b1d8f; + text-decoration: none; + // transition: all 250ms ease; + transition: all 250ms cubic-bezier(.2,0,.4,2); + &:hover { + background-color: rgba(255,255,255,1); + box-shadow: 0 0 0 2px #6b1d8f; + } + &.active { + background-color: #6b1d8f; + color: white; + } + } + } + } + + > main { + flex: 1; + padding: 10px; + } +} + + +h1 { + margin: 0; + color: #6b1d8f; + font-weight: 300; + font-size: 250%; +} + + +input, textarea { + box-sizing: border-box; + margin: 1px; + padding: .25em .5em; + background: #fff; + border: 1px solid #999; + border-radius: 3px; + font: inherit; + color: #000; + outline: none; + + &:focus { + border-color: #6b1d8f; + } +} + + +button, input[type="submit"], input[type="reset"], input[type="button"] { + box-sizing: border-box; + margin: 1px; + padding: .25em .8em; + background: #6b1d8f; + border: 1px solid #6b1d8f; + // border: none; + border-radius: 1.5em; + font: inherit; + color: white; + outline: none; + cursor: pointer; +} + + +.cursor { + position: absolute; + left: 0; + top: 0; + width: 8px; + height: 8px; + margin: -5px 0 0 -5px; + border: 2px solid #F00; + border-radius: 50%; + transform-origin: 50% 50%; + pointer-events: none; + overflow: hidden; + font-size: 9px; + line-height: 25px; + text-indent: 15px; + white-space: nowrap; + + &:not(.label) { + contain: strict; + } + + &.label { + overflow: visible; + } + + // &.big { + // transform: scale(2); + // // width: 24px; + // // height: 24px; + // // margin: -13px 0 0 -13px; + // } + + .label { + position: absolute; + left: 0; + top: 0; + //transform: translateZ(0); + // z-index: 10; + } +} + + +.animation-picker { + position: fixed; + display: inline-block; + right: 0; + top: 0; + padding: 10px; + background: #000; + color: #BBB; + z-index: 1000; + + select { + font-size: 100%; + margin-left: 5px; + } +} diff --git a/preact/demo/styled-components.js b/preact/demo/styled-components.js new file mode 100644 index 0000000..f8433ba --- /dev/null +++ b/preact/demo/styled-components.js @@ -0,0 +1,31 @@ +import { createElement } from 'preact'; +import styled, { css } from 'styled-components'; + +const Button = styled.button` + background: transparent; + border-radius: 3px; + border: 2px solid palevioletred; + color: palevioletred; + margin: 0.5em 1em; + padding: 0.25em 1em; + + ${props => + props.primary && + css` + background: palevioletred; + color: white; + `} +`; + +const Container = styled.div` + text-align: center; +`; + +export default function StyledComp() { + return ( + <Container> + <Button>Normal Button</Button> + <Button primary>Primary Button</Button> + </Container> + ); +} diff --git a/preact/demo/suspense-router/bye.js b/preact/demo/suspense-router/bye.js new file mode 100644 index 0000000..fe2a49c --- /dev/null +++ b/preact/demo/suspense-router/bye.js @@ -0,0 +1,12 @@ +import { createElement } from 'react'; +import { Link } from './simple-router'; + +/** @jsx createElement */ + +export default function Bye() { + return ( + <div> + Bye! <Link to="/">Go to Hello!</Link> + </div> + ); +} diff --git a/preact/demo/suspense-router/hello.js b/preact/demo/suspense-router/hello.js new file mode 100644 index 0000000..2566dd3 --- /dev/null +++ b/preact/demo/suspense-router/hello.js @@ -0,0 +1,12 @@ +import { createElement } from 'react'; +import { Link } from './simple-router'; + +/** @jsx createElement */ + +export default function Hello() { + return ( + <div> + Hello! <Link to="/bye">Go to Bye!</Link> + </div> + ); +} diff --git a/preact/demo/suspense-router/index.js b/preact/demo/suspense-router/index.js new file mode 100644 index 0000000..2572209 --- /dev/null +++ b/preact/demo/suspense-router/index.js @@ -0,0 +1,30 @@ +import { createElement, Suspense, lazy } from 'react'; + +import { Router, Route, Switch } from './simple-router'; + +/** @jsx createElement */ + +let Hello = lazy(() => import('./hello.js')); +let Bye = lazy(() => import('./bye.js')); + +function Loading() { + return <div>Hey! This is a fallback because we're loading things! :D</div>; +} + +export default function SuspenseRouterBug() { + return ( + <Router> + <h1>Suspense Router bug</h1> + <Suspense fallback={<Loading />}> + <Switch> + <Route path="/" exact> + <Hello /> + </Route> + <Route path="/bye"> + <Bye /> + </Route> + </Switch> + </Suspense> + </Router> + ); +} diff --git a/preact/demo/suspense-router/simple-router.js b/preact/demo/suspense-router/simple-router.js new file mode 100644 index 0000000..ed48ea6 --- /dev/null +++ b/preact/demo/suspense-router/simple-router.js @@ -0,0 +1,87 @@ +import { + createElement, + cloneElement, + createContext, + useState, + useContext, + Children, + useLayoutEffect +} from 'react'; + +/** @jsx createElement */ + +const memoryHistory = { + /** + * @typedef {{ pathname: string }} Location + * @typedef {(location: Location) => void} HistoryListener + * @type {HistoryListener[]} + */ + listeners: [], + + /** + * @param {HistoryListener} listener + */ + listen(listener) { + const newLength = this.listeners.push(listener); + return () => this.listeners.splice(newLength - 1, 1); + }, + + /** + * @param {Location} to + */ + navigate(to) { + this.listeners.forEach(listener => listener(to)); + } +}; + +/** @type {import('react').Context<{ history: typeof memoryHistory; location: Location }>} */ +const RouterContext = createContext(null); + +export function Router({ history = memoryHistory, children }) { + const [location, setLocation] = useState({ pathname: '/' }); + + useLayoutEffect(() => { + return history.listen(newLocation => setLocation(newLocation)); + }, []); + + return ( + <RouterContext.Provider value={{ history, location }}> + {children} + </RouterContext.Provider> + ); +} + +export function Switch(props) { + const { location } = useContext(RouterContext); + + let element = null; + Children.forEach(props.children, child => { + if (element == null && child.props.path == location.pathname) { + element = child; + } + }); + + return element; +} + +/** + * @param {{ children: any; path: string; exact?: boolean; }} props + */ +export function Route({ children, path, exact }) { + return children; +} + +export function Link({ to, children }) { + const { history } = useContext(RouterContext); + const onClick = event => { + event.preventDefault(); + event.stopPropagation(); + history.navigate({ pathname: to }); + }; + + return ( + <a href={to} onClick={onClick}> + {children} + </a> + ); +} diff --git a/preact/demo/suspense.js b/preact/demo/suspense.js new file mode 100644 index 0000000..9b620fa --- /dev/null +++ b/preact/demo/suspense.js @@ -0,0 +1,97 @@ +// eslint-disable-next-line no-unused-vars +import { + createElement, + Component, + memo, + Fragment, + Suspense, + lazy +} from 'react'; + +function LazyComp() { + return <div>I'm (fake) lazy loaded</div>; +} + +const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); + +function createSuspension(name, timeout, error) { + let done = false; + let prom; + + return { + name, + timeout, + start: () => { + if (!prom) { + prom = new Promise((res, rej) => { + setTimeout(() => { + done = true; + if (error) { + rej(error); + } else { + res(); + } + }, timeout); + }); + } + + return prom; + }, + getPromise: () => prom, + isDone: () => done + }; +} + +function CustomSuspense({ isDone, start, timeout, name }) { + if (!isDone()) { + throw start(); + } + + return ( + <div> + Hello from CustomSuspense {name}, loaded after {timeout / 1000}s + </div> + ); +} + +function init() { + return { + s1: createSuspension('1', 1000, null), + s2: createSuspension('2', 2000, null), + s3: createSuspension('3', 3000, null) + }; +} + +export default class DevtoolsDemo extends Component { + constructor(props) { + super(props); + this.state = init(); + this.onRerun = this.onRerun.bind(this); + } + + onRerun() { + this.setState(init()); + } + + render(props, state) { + return ( + <div> + <h1>lazy()</h1> + <Suspense fallback={<div>Loading (fake) lazy loaded component...</div>}> + <Lazy /> + </Suspense> + <h1>Suspense</h1> + <div> + <button onClick={this.onRerun}>Rerun</button> + </div> + <Suspense fallback={<div>Fallback 1</div>}> + <CustomSuspense {...state.s1} /> + <Suspense fallback={<div>Fallback 2</div>}> + <CustomSuspense {...state.s2} /> + <CustomSuspense {...state.s3} /> + </Suspense> + </Suspense> + </div> + ); + } +} diff --git a/preact/demo/textFields.js b/preact/demo/textFields.js new file mode 100644 index 0000000..58f69f1 --- /dev/null +++ b/preact/demo/textFields.js @@ -0,0 +1,37 @@ +import React, { useState } from 'react'; +import TextField from '@material-ui/core/TextField'; + +/** @jsx React.createElement */ + +const PatchedTextField = props => { + const [value, set] = useState(props.value); + return ( + <TextField {...props} value={value} onChange={e => set(e.target.value)} /> + ); +}; + +const TextFields = () => ( + <div> + <TextField + variant="outlined" + margin="normal" + fullWidth + label="Cannot type in" + /> + <PatchedTextField + variant="outlined" + margin="normal" + fullWidth + label="I can" + /> + <TextField + defaultValue="Reset after blur or empty" + variant="outlined" + margin="normal" + fullWidth + label="default value" + /> + </div> +); + +export default TextFields; diff --git a/preact/demo/todo.js b/preact/demo/todo.js new file mode 100644 index 0000000..189b4e8 --- /dev/null +++ b/preact/demo/todo.js @@ -0,0 +1,47 @@ +import { createElement, Component } from 'preact'; + +let counter = 0; + +export default class TodoList extends Component { + state = { todos: [], text: '' }; + + setText = e => { + this.setState({ text: e.target.value }); + }; + + addTodo = () => { + let { todos, text } = this.state; + todos = todos.concat({ text, id: ++counter }); + this.setState({ todos, text: '' }); + }; + + removeTodo = e => { + let id = e.target.getAttribute('data-id'); + this.setState({ todos: this.state.todos.filter(t => t.id != id) }); + }; + + render({}, { todos, text }) { + return ( + <form onSubmit={this.addTodo} action="javascript:"> + <input value={text} onInput={this.setText} /> + <button type="submit">Add</button> + <ul> + <TodoItems todos={todos} removeTodo={this.removeTodo} /> + </ul> + </form> + ); + } +} + +class TodoItems extends Component { + render({ todos, removeTodo }) { + return todos.map(todo => ( + <li key={todo.id}> + <button type="button" onClick={removeTodo} data-id={todo.id}> + × + </button>{' '} + {todo.text} + </li> + )); + } +} diff --git a/preact/demo/tsconfig.json b/preact/demo/tsconfig.json new file mode 100644 index 0000000..b252efc --- /dev/null +++ b/preact/demo/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "jsx": "react", + "jsxFactory": "h", + "baseUrl": ".", + "target": "es2018", + "module": "es2015", + "moduleResolution": "node", + "paths": { + "preact/hooks": ["../hooks/src/index.js"], + "preact": ["../src/index.js"], + } + } +} diff --git a/preact/demo/webpack.config.js b/preact/demo/webpack.config.js new file mode 100644 index 0000000..063f507 --- /dev/null +++ b/preact/demo/webpack.config.js @@ -0,0 +1,112 @@ +/* eslint-disable */ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +const preact = path.join(__dirname, '..', 'src'); +const compat = path.join(__dirname, '..', 'compat', 'src'); + +module.exports = { + context: __dirname, + entry: './index', + output: { + publicPath: '/' + }, + resolve: { + alias: { + ['preact/debug']: path.join(__dirname, '..', 'debug'), + ['preact/devtools']: path.join(__dirname, '..', 'devtools'), + ['preact/hooks']: path.join(__dirname, '..', 'hooks', 'src'), + preact: preact, + react: compat, + 'react-dom': compat + }, + extensions: ['.tsx', '.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: 'babel-loader', + options: { + sourceMap: true, + presets: [ + [require.resolve('@babel/preset-typescript'), { jsxPragma: 'h' }], + [ + require.resolve('@babel/preset-env'), + { + targets: { + browsers: ['last 2 versions', 'IE >= 9'] + }, + modules: false, + loose: true + } + ], + [require.resolve('@babel/preset-react')] + ], + plugins: [ + [require.resolve('@babel/plugin-transform-runtime')], + [require.resolve('@babel/plugin-transform-react-jsx-source')], + [ + require.resolve('@babel/plugin-transform-react-jsx'), + { pragma: 'h', pragmaFrag: 'Fragment' } + ], + [ + require.resolve('@babel/plugin-proposal-decorators'), + { legacy: true } + ], + [ + require.resolve('@babel/plugin-proposal-class-properties'), + { loose: true } + ] + ] + } + }, + { + test: /\.js$/, + loader: 'babel-loader', + options: { + sourceMap: true, + presets: [ + [ + require.resolve('@babel/preset-env'), + { + targets: { + browsers: ['last 2 versions', 'IE >= 9'] + }, + modules: false, + loose: true + } + ], + [require.resolve('@babel/preset-react')] + ], + plugins: [ + [require.resolve('@babel/plugin-transform-react-jsx-source')], + [ + require.resolve('@babel/plugin-transform-react-jsx'), + { pragma: 'createElement', pragmaFrag: 'Fragment' } + ], + [require.resolve('@babel/plugin-proposal-class-properties')], + [ + require.resolve('@babel/plugin-transform-react-constant-elements') + ], + [require.resolve('@babel/plugin-syntax-dynamic-import')] + ] + } + }, + { + test: /\.s?css$/, + use: ['style-loader', 'css-loader', 'sass-loader'] + } + ] + }, + devtool: 'inline-source-map', + node: { + process: 'mock', + Buffer: false, + setImmediate: false + }, + devServer: { + historyApiFallback: true + }, + plugins: [new HtmlWebpackPlugin()] +}; |