├── test ├── node │ └── index.js ├── polyfills.js ├── shared │ ├── exports.js │ └── h.js ├── browser │ ├── keys.js │ ├── spec.js │ ├── linked-state.js │ ├── svg.js │ ├── context.js │ ├── performance.js │ ├── devtools.js │ ├── refs.js │ ├── lifecycle.js │ └── render.js └── karma.conf.js ├── devtools ├── index.js └── devtools.js ├── typings.json ├── .gitignore ├── .editorconfig ├── src ├── clone-element.js ├── preact.js.flow ├── vnode.js ├── preact.js ├── render-queue.js ├── render.js ├── options.js ├── dom │ ├── recycler.js │ └── index.js ├── constants.js ├── vdom │ ├── component-recycler.js │ ├── functional-component.js │ ├── index.js │ ├── component.js │ └── diff.js ├── linked-state.js ├── h.js ├── util.js ├── component.js └── preact.d.ts ├── config ├── rollup.config.aliases.js ├── rollup.config.devtools.js ├── rollup.config.js ├── codemod-const.js ├── codemod-strip-tdz.js └── eslint-config.js ├── .travis.yml ├── LICENSE ├── package.json └── README.md /test/node/index.js: -------------------------------------------------------------------------------- 1 | // this is just a placeholder 2 | -------------------------------------------------------------------------------- /devtools/index.js: -------------------------------------------------------------------------------- 1 | import { initDevTools } from './devtools'; 2 | 3 | initDevTools(); 4 | 5 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact", 3 | "main": "src/preact.d.ts", 4 | "version": false 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | .DS_Store 4 | /dist 5 | /_dev 6 | /coverage 7 | 8 | # Additional bundles 9 | /*.js 10 | /*.js.map 11 | -------------------------------------------------------------------------------- /test/polyfills.js: -------------------------------------------------------------------------------- 1 | // ES2015 APIs used by developer tools integration 2 | import 'core-js/es6/map'; 3 | import 'core-js/fn/array/fill'; 4 | import 'core-js/fn/array/from'; 5 | import 'core-js/fn/object/assign'; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,.*rc,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/clone-element.js: -------------------------------------------------------------------------------- 1 | import { clone, extend } from './util'; 2 | import { h } from './h'; 3 | 4 | export function cloneElement(vnode, props) { 5 | return h( 6 | vnode.nodeName, 7 | extend(clone(vnode.attributes), props), 8 | arguments.length>2 ? [].slice.call(arguments, 2) : vnode.children 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/preact.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { createElement as h, cloneElement, Component, render } from 'react'; 4 | 5 | export { h, cloneElement, Component, render }; 6 | export default { h, cloneElement, Component, render }; 7 | 8 | declare export function rerender(): void; 9 | declare export var options: Object; 10 | -------------------------------------------------------------------------------- /config/rollup.config.aliases.js: -------------------------------------------------------------------------------- 1 | import memory from 'rollup-plugin-memory'; 2 | import rollupConfig from './rollup.config'; 3 | 4 | export default Object.assign({}, rollupConfig, { 5 | plugins: [ 6 | memory({ 7 | path: 'src/preact', 8 | contents: `import { h } from './preact';export * from './preact';export { h as createElement };` 9 | }), 10 | ...rollupConfig.plugins.slice(1) 11 | ] 12 | }); 13 | -------------------------------------------------------------------------------- /src/vnode.js: -------------------------------------------------------------------------------- 1 | /** Virtual DOM Node */ 2 | export function VNode(nodeName, attributes, children) { 3 | /** @type {string|function} */ 4 | this.nodeName = nodeName; 5 | 6 | /** @type {object|undefined} */ 7 | this.attributes = attributes; 8 | 9 | /** @type {array|undefined} */ 10 | this.children = children; 11 | 12 | /** Reference to the given key. */ 13 | this.key = attributes && attributes.key; 14 | } 15 | -------------------------------------------------------------------------------- /config/rollup.config.devtools.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | 4 | export default { 5 | entry: 'devtools/index.js', 6 | external: ['preact'], 7 | format: 'umd', 8 | globals: { 9 | preact: 'preact' 10 | }, 11 | moduleName: 'preactDevTools', 12 | plugins: [ 13 | babel({ 14 | sourceMap: true, 15 | loose: 'all', 16 | blacklist: ['es6.tailCall'], 17 | exclude: 'node_modules/**' 18 | }) 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/preact.js: -------------------------------------------------------------------------------- 1 | import { h } from './h'; 2 | import { cloneElement } from './clone-element'; 3 | import { Component } from './component'; 4 | import { render } from './render'; 5 | import { rerender } from './render-queue'; 6 | import options from './options'; 7 | 8 | export default { 9 | h, 10 | cloneElement, 11 | Component, 12 | render, 13 | rerender, 14 | options 15 | }; 16 | 17 | export { 18 | h, 19 | cloneElement, 20 | Component, 21 | render, 22 | rerender, 23 | options 24 | }; 25 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import memory from 'rollup-plugin-memory'; 4 | 5 | export default { 6 | exports: 'named', 7 | useStrict: false, 8 | plugins: [ 9 | memory({ 10 | path: 'src/preact', 11 | contents: "export * from './preact';" 12 | }), 13 | nodeResolve({ 14 | main: true 15 | }), 16 | babel({ 17 | sourceMap: true, 18 | loose: 'all', 19 | blacklist: ['es6.tailCall'], 20 | exclude: 'node_modules/**' 21 | }) 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /src/render-queue.js: -------------------------------------------------------------------------------- 1 | import options from './options'; 2 | import { defer } from './util'; 3 | import { renderComponent } from './vdom/component'; 4 | 5 | /** Managed queue of dirty components to be re-rendered */ 6 | 7 | // items/itemsOffline swap on each rerender() call (just a simple pool technique) 8 | let items = []; 9 | 10 | export function enqueueRender(component) { 11 | if (!component._dirty && (component._dirty = true) && items.push(component)==1) { 12 | (options.debounceRendering || defer)(rerender); 13 | } 14 | } 15 | 16 | 17 | export function rerender() { 18 | let p, list = items; 19 | items = []; 20 | while ( (p = list.pop()) ) { 21 | if (p._dirty) renderComponent(p); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import { diff } from './vdom/diff'; 2 | 3 | /** Render JSX into a `parent` Element. 4 | * @param {VNode} vnode A (JSX) VNode to render 5 | * @param {Element} parent DOM element to render into 6 | * @param {Element} [merge] Attempt to re-use an existing DOM tree rooted at `merge` 7 | * @public 8 | * 9 | * @example 10 | * // render a div into : 11 | * render(
hello!
, document.body); 12 | * 13 | * @example 14 | * // render a "Thing" component into #foo: 15 | * const Thing = ({ name }) => { name }; 16 | * render(, document.querySelector('#foo')); 17 | */ 18 | export function render(vnode, parent, merge) { 19 | return diff(merge, vnode, {}, false, parent); 20 | } 21 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | /** Global options 2 | * @public 3 | * @namespace options {Object} 4 | */ 5 | export default { 6 | 7 | /** If `true`, `prop` changes trigger synchronous component updates. 8 | * @name syncComponentUpdates 9 | * @type Boolean 10 | * @default true 11 | */ 12 | //syncComponentUpdates: true, 13 | 14 | /** Processes all created VNodes. 15 | * @param {VNode} vnode A newly-created VNode to normalize/process 16 | */ 17 | //vnode(vnode) { } 18 | 19 | /** Hook invoked after a component is mounted. */ 20 | // afterMount(component) { } 21 | 22 | /** Hook invoked after the DOM is updated with a component's latest render. */ 23 | // afterUpdate(component) { } 24 | 25 | /** Hook invoked immediately before a component is unmounted. */ 26 | // beforeUnmount(component) { } 27 | }; 28 | -------------------------------------------------------------------------------- /src/dom/recycler.js: -------------------------------------------------------------------------------- 1 | import { toLowerCase } from '../util'; 2 | import { removeNode } from './index'; 3 | 4 | /** DOM node pool, keyed on nodeName. */ 5 | 6 | const nodes = {}; 7 | 8 | export function collectNode(node) { 9 | removeNode(node); 10 | 11 | if (node instanceof Element) { 12 | node._component = node._componentConstructor = null; 13 | 14 | let name = node.normalizedNodeName || toLowerCase(node.nodeName); 15 | (nodes[name] || (nodes[name] = [])).push(node); 16 | } 17 | } 18 | 19 | 20 | export function createNode(nodeName, isSvg) { 21 | let name = toLowerCase(nodeName), 22 | node = nodes[name] && nodes[name].pop() || (isSvg ? document.createElementNS('http://www.w3.org/2000/svg', nodeName) : document.createElement(nodeName)); 23 | node.normalizedNodeName = name; 24 | return node; 25 | } 26 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // render modes 2 | 3 | export const NO_RENDER = 0; 4 | export const SYNC_RENDER = 1; 5 | export const FORCE_RENDER = 2; 6 | export const ASYNC_RENDER = 3; 7 | 8 | export const EMPTY = {}; 9 | 10 | export const ATTR_KEY = typeof Symbol!=='undefined' ? Symbol.for('preactattr') : '__preactattr_'; 11 | 12 | // DOM properties that should NOT have "px" added when numeric 13 | export const NON_DIMENSION_PROPS = { 14 | boxFlex:1, boxFlexGroup:1, columnCount:1, fillOpacity:1, flex:1, flexGrow:1, 15 | flexPositive:1, flexShrink:1, flexNegative:1, fontWeight:1, lineClamp:1, lineHeight:1, 16 | opacity:1, order:1, orphans:1, strokeOpacity:1, widows:1, zIndex:1, zoom:1 17 | }; 18 | 19 | // DOM event types that do not bubble and should be attached via useCapture 20 | export const NON_BUBBLING_EVENTS = { blur:1, error:1, focus:1, load:1, resize:1, scroll:1 }; 21 | -------------------------------------------------------------------------------- /test/shared/exports.js: -------------------------------------------------------------------------------- 1 | import preact, { h, Component, render, rerender, options } from '../../src/preact'; 2 | import { expect } from 'chai'; 3 | 4 | describe('preact', () => { 5 | it('should be available as a default export', () => { 6 | expect(preact).to.be.an('object'); 7 | expect(preact).to.have.property('h', h); 8 | expect(preact).to.have.property('Component', Component); 9 | expect(preact).to.have.property('render', render); 10 | expect(preact).to.have.property('rerender', rerender); 11 | expect(preact).to.have.property('options', options); 12 | }); 13 | 14 | it('should be available as named exports', () => { 15 | expect(h).to.be.a('function'); 16 | expect(Component).to.be.a('function'); 17 | expect(render).to.be.a('function'); 18 | expect(rerender).to.be.a('function'); 19 | expect(options).to.exist.and.be.an('object'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "6" 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | # Make chrome browser available for testing 13 | before_install: 14 | - export CHROME_BIN=chromium-browser 15 | - export DISPLAY=:99.0 16 | - sh -e /etc/init.d/xvfb start 17 | 18 | install: 19 | - npm install 20 | 21 | script: 22 | - npm run build 23 | - npm run test 24 | - BROWSER=true COVERAGE=false FLAKEY=false PERFORMANCE=false npm run test:karma 25 | - ./node_modules/coveralls/bin/coveralls.js < ./coverage/lcov.info 26 | 27 | # Necessary to compile native modules for io.js v3 or Node.js v4 28 | env: 29 | - CXX=g++-4.8 30 | 31 | # Necessary to compile native modules for io.js v3 or Node.js v4 32 | addons: 33 | apt: 34 | sources: 35 | - ubuntu-toolchain-r-test 36 | packages: 37 | - g++-4.8 38 | -------------------------------------------------------------------------------- /src/vdom/component-recycler.js: -------------------------------------------------------------------------------- 1 | import { Component } from '../component'; 2 | 3 | /** Retains a pool of Components for re-use, keyed on component name. 4 | * Note: since component names are not unique or even necessarily available, these are primarily a form of sharding. 5 | * @private 6 | */ 7 | const components = {}; 8 | 9 | 10 | export function collectComponent(component) { 11 | let name = component.constructor.name, 12 | list = components[name]; 13 | if (list) list.push(component); 14 | else components[name] = [component]; 15 | } 16 | 17 | 18 | export function createComponent(Ctor, props, context) { 19 | let inst = new Ctor(props, context), 20 | list = components[Ctor.name]; 21 | Component.call(inst, props, context); 22 | if (list) { 23 | for (let i=list.length; i--; ) { 24 | if (list[i].constructor===Ctor) { 25 | inst.nextBase = list[i].nextBase; 26 | list.splice(i, 1); 27 | break; 28 | } 29 | } 30 | } 31 | return inst; 32 | } 33 | -------------------------------------------------------------------------------- /src/linked-state.js: -------------------------------------------------------------------------------- 1 | import { isString, delve } from './util'; 2 | 3 | /** Create an Event handler function that sets a given state property. 4 | * @param {Component} component The component whose state should be updated 5 | * @param {string} key A dot-notated key path to update in the component's state 6 | * @param {string} eventPath A dot-notated key path to the value that should be retrieved from the Event or component 7 | * @returns {function} linkedStateHandler 8 | * @private 9 | */ 10 | export function createLinkedState(component, key, eventPath) { 11 | let path = key.split('.'); 12 | return function(e) { 13 | let t = e && e.target || this, 14 | state = {}, 15 | obj = state, 16 | v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? (t.type.match(/^che|rad/) ? t.checked : t.value) : e, 17 | i = 0; 18 | for ( ; i { 7 | let j = api.jscodeshift, 8 | code = j(file.source), 9 | constants = {}, 10 | found = 0; 11 | 12 | code.find(j.VariableDeclaration) 13 | .filter( decl => { 14 | for (let i=decl.value.declarations.length; i--; ) { 15 | let node = decl.value.declarations[i], 16 | name = node.id && node.id.name, 17 | init = node.init; 18 | if (name && init && name.match(/^[A-Z0-9_$]+$/g)) { 19 | if (init.type==='Literal') { 20 | console.log(`Inlining constant: ${name}=${init.raw}`); 21 | found++; 22 | constants[name] = init; 23 | // remove declaration 24 | decl.value.declarations.splice(i, 1); 25 | // if it's the last, we'll remove the whole statement 26 | return !decl.value.declarations.length; 27 | } 28 | } 29 | } 30 | return false; 31 | }) 32 | .remove(); 33 | 34 | code.find(j.Identifier) 35 | .filter( path => path.value.name && constants.hasOwnProperty(path.value.name) ) 36 | .replaceWith( path => (found++, constants[path.value.name]) ); 37 | 38 | return found ? code.toSource({ quote: 'single' }) : null; 39 | }; 40 | -------------------------------------------------------------------------------- /src/h.js: -------------------------------------------------------------------------------- 1 | import { VNode } from './vnode'; 2 | import options from './options'; 3 | 4 | 5 | const stack = []; 6 | 7 | const EMPTY_CHILDREN = []; 8 | 9 | /** JSX/hyperscript reviver 10 | * Benchmarks: https://esbench.com/bench/57ee8f8e330ab09900a1a1a0 11 | * @see http://jasonformat.com/wtf-is-jsx 12 | * @public 13 | * @example 14 | * /** @jsx h *\/ 15 | * import { render, h } from 'preact'; 16 | * render(foo, document.body); 17 | */ 18 | export function h(nodeName, attributes) { 19 | let children, lastSimple, child, simple, i; 20 | for (i=arguments.length; i-- > 2; ) { 21 | stack.push(arguments[i]); 22 | } 23 | if (attributes && attributes.children) { 24 | if (!stack.length) stack.push(attributes.children); 25 | delete attributes.children; 26 | } 27 | while (stack.length) { 28 | if ((child = stack.pop()) instanceof Array) { 29 | for (i=child.length; i--; ) stack.push(child[i]); 30 | } 31 | else if (child!=null && child!==true && child!==false) { 32 | if (typeof child=='number') child = String(child); 33 | simple = typeof child=='string'; 34 | if (simple && lastSimple) { 35 | children[children.length-1] += child; 36 | } 37 | else { 38 | (children || (children = [])).push(child); 39 | lastSimple = simple; 40 | } 41 | } 42 | } 43 | 44 | let p = new VNode(nodeName, attributes || undefined, children || EMPTY_CHILDREN); 45 | 46 | // if a "vnode hook" is defined, pass every created VNode to it 47 | if (options.vnode) options.vnode(p); 48 | 49 | return p; 50 | } 51 | -------------------------------------------------------------------------------- /src/vdom/index.js: -------------------------------------------------------------------------------- 1 | import { clone, isString, isFunction, toLowerCase } from '../util'; 2 | import { isFunctionalComponent } from './functional-component'; 3 | 4 | 5 | /** Check if two nodes are equivalent. 6 | * @param {Element} node 7 | * @param {VNode} vnode 8 | * @private 9 | */ 10 | export function isSameNodeType(node, vnode) { 11 | if (isString(vnode)) { 12 | return node instanceof Text; 13 | } 14 | if (isString(vnode.nodeName)) { 15 | return !node._componentConstructor && isNamedNode(node, vnode.nodeName); 16 | } 17 | if (isFunction(vnode.nodeName)) { 18 | return (node._componentConstructor ? node._componentConstructor===vnode.nodeName : true) || isFunctionalComponent(vnode); 19 | } 20 | } 21 | 22 | 23 | export function isNamedNode(node, nodeName) { 24 | return node.normalizedNodeName===nodeName || toLowerCase(node.nodeName)===toLowerCase(nodeName); 25 | } 26 | 27 | 28 | /** 29 | * Reconstruct Component-style `props` from a VNode. 30 | * Ensures default/fallback values from `defaultProps`: 31 | * Own-properties of `defaultProps` not present in `vnode.attributes` are added. 32 | * @param {VNode} vnode 33 | * @returns {Object} props 34 | */ 35 | export function getNodeProps(vnode) { 36 | let props = clone(vnode.attributes); 37 | props.children = vnode.children; 38 | 39 | let defaultProps = vnode.nodeName.defaultProps; 40 | if (defaultProps) { 41 | for (let i in defaultProps) { 42 | if (props[i]===undefined) { 43 | props[i] = defaultProps[i]; 44 | } 45 | } 46 | } 47 | 48 | return props; 49 | } 50 | -------------------------------------------------------------------------------- /config/codemod-strip-tdz.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | 4 | // parent node types that we don't want to remove pointless initializations from (because it breaks hoisting) 5 | const BLOCKED = ['ForStatement', 'WhileStatement']; // 'IfStatement', 'SwitchStatement' 6 | 7 | /** Removes var initialization to `void 0`, which Babel adds for TDZ strictness. */ 8 | export default (file, api) => { 9 | let { jscodeshift } = api, 10 | found = 0; 11 | 12 | let code = jscodeshift(file.source) 13 | .find(jscodeshift.VariableDeclaration) 14 | .forEach(handleDeclaration); 15 | 16 | function handleDeclaration(decl) { 17 | let p = decl, 18 | remove = true; 19 | 20 | while ((p = p.parentPath)) { 21 | if (~BLOCKED.indexOf(p.value.type)) { 22 | remove = false; 23 | break; 24 | } 25 | } 26 | 27 | decl.value.declarations.filter(isPointless).forEach( node => { 28 | if (remove===false) { 29 | console.log(`> Skipping removal of undefined init for "${node.id.name}": within ${p.value.type}`); 30 | } 31 | else { 32 | removeNodeInitialization(node); 33 | } 34 | }); 35 | } 36 | 37 | function removeNodeInitialization(node) { 38 | node.init = null; 39 | found++; 40 | } 41 | 42 | function isPointless(node) { 43 | let { init } = node; 44 | if (init) { 45 | if (init.type==='UnaryExpression' && init.operator==='void' && init.argument.value==0) { 46 | return true; 47 | } 48 | if (init.type==='Identifier' && init.name==='undefined') { 49 | return true; 50 | } 51 | } 52 | return false; 53 | } 54 | 55 | return found ? code.toSource({ quote: 'single' }) : null; 56 | }; 57 | -------------------------------------------------------------------------------- /config/eslint-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: 'eslint:recommended', 4 | plugins: [ 5 | 'react' 6 | ], 7 | env: { 8 | browser: true, 9 | mocha: true, 10 | node: true, 11 | es6: true 12 | }, 13 | parserOptions: { 14 | ecmaFeatures: { 15 | modules: true, 16 | jsx: true 17 | } 18 | }, 19 | globals: { 20 | sinon: true, 21 | expect: true 22 | }, 23 | rules: { 24 | 'react/jsx-uses-react': 2, 25 | 'react/jsx-uses-vars': 2, 26 | 'no-unused-vars': [1, { varsIgnorePattern: '^h$' }], 27 | 'no-cond-assign': 1, 28 | 'no-empty': 0, 29 | 'no-console': 1, 30 | semi: 2, 31 | camelcase: 0, 32 | 'comma-style': 2, 33 | 'comma-dangle': [2, 'never'], 34 | indent: [2, 'tab', {SwitchCase: 1}], 35 | 'no-mixed-spaces-and-tabs': [2, 'smart-tabs'], 36 | 'no-trailing-spaces': [2, { skipBlankLines: true }], 37 | 'max-nested-callbacks': [2, 3], 38 | 'no-eval': 2, 39 | 'no-implied-eval': 2, 40 | 'no-new-func': 2, 41 | 'guard-for-in': 0, 42 | eqeqeq: 0, 43 | 'no-else-return': 2, 44 | 'no-redeclare': 2, 45 | 'no-dupe-keys': 2, 46 | radix: 2, 47 | strict: [2, 'never'], 48 | 'no-shadow': 0, 49 | 'callback-return': [1, ['callback', 'cb', 'next', 'done']], 50 | 'no-delete-var': 2, 51 | 'no-undef-init': 2, 52 | 'no-shadow-restricted-names': 2, 53 | 'handle-callback-err': 0, 54 | 'no-lonely-if': 2, 55 | 'keyword-spacing': 2, 56 | 'constructor-super': 2, 57 | 'no-this-before-super': 2, 58 | 'no-dupe-class-members': 2, 59 | 'no-const-assign': 2, 60 | 'prefer-spread': 2, 61 | 'no-useless-concat': 2, 62 | 'no-var': 2, 63 | 'object-shorthand': 2, 64 | 'prefer-arrow-callback': 2 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** Copy own-properties from `props` onto `obj`. 2 | * @returns obj 3 | * @private 4 | */ 5 | export function extend(obj, props) { 6 | if (props) { 7 | for (let i in props) obj[i] = props[i]; 8 | } 9 | return obj; 10 | } 11 | 12 | 13 | /** Fast clone. Note: does not filter out non-own properties. 14 | * @see https://esbench.com/bench/56baa34f45df6895002e03b6 15 | */ 16 | export function clone(obj) { 17 | return extend({}, obj); 18 | } 19 | 20 | 21 | /** Get a deep property value from the given object, expressed in dot-notation. 22 | * @private 23 | */ 24 | export function delve(obj, key) { 25 | for (let p=key.split('.'), i=0; i lcCache[s] || (lcCache[s] = s.toLowerCase()); 62 | 63 | 64 | /** Call a function asynchronously, as soon as possible. 65 | * @param {Function} callback 66 | */ 67 | let resolved = typeof Promise!=='undefined' && Promise.resolve(); 68 | export const defer = resolved ? (f => { resolved.then(f); }) : setTimeout; 69 | -------------------------------------------------------------------------------- /test/browser/keys.js: -------------------------------------------------------------------------------- 1 | import { h, Component, render } from '../../src/preact'; 2 | /** @jsx h */ 3 | 4 | describe('keys', () => { 5 | let scratch; 6 | 7 | before( () => { 8 | scratch = document.createElement('div'); 9 | (document.body || document.documentElement).appendChild(scratch); 10 | }); 11 | 12 | beforeEach( () => { 13 | scratch.innerHTML = ''; 14 | }); 15 | 16 | after( () => { 17 | scratch.parentNode.removeChild(scratch); 18 | scratch = null; 19 | }); 20 | 21 | // See developit/preact-compat#21 22 | it('should remove orphaned keyed nodes', () => { 23 | const root = render(( 24 |
25 |
1
26 |
  • a
  • 27 |
    28 | ), scratch); 29 | 30 | render(( 31 |
    32 |
    2
    33 |
  • b
  • 34 |
    35 | ), scratch, root); 36 | 37 | expect(scratch.innerHTML).to.equal('
    2
  • b
  • '); 38 | }); 39 | 40 | it('should set VNode#key property', () => { 41 | expect(
    ).to.have.property('key').that.is.empty; 42 | expect(
    ).to.have.property('key').that.is.empty; 43 | expect(
    ).to.have.property('key', '1'); 44 | }); 45 | 46 | it('should remove keyed nodes (#232)', () => { 47 | class App extends Component { 48 | componentDidMount() { 49 | setTimeout(() => this.setState({opened: true,loading: true}), 10); 50 | setTimeout(() => this.setState({opened: true,loading: false}), 20); 51 | } 52 | 53 | render({ opened, loading }) { 54 | return ( 55 | 56 |
    This div needs to be here for this to break
    57 | { opened && !loading &&
    {[]}
    } 58 |
    59 | ); 60 | } 61 | } 62 | 63 | class BusyIndicator extends Component { 64 | render({ children, busy }) { 65 | return
    66 | { children && children.length ? children :
    } 67 |
    68 |
    indicator
    69 |
    indicator
    70 |
    indicator
    71 |
    72 |
    ; 73 | } 74 | } 75 | 76 | let root; 77 | 78 | root = render(, scratch, root); 79 | root = render(, scratch, root); 80 | root = render(, scratch, root); 81 | 82 | let html = String(root.innerHTML).replace(/ class=""/g, ''); 83 | expect(html).to.equal('
    This div needs to be here for this to break
    indicator
    indicator
    indicator
    '); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/dom/index.js: -------------------------------------------------------------------------------- 1 | import { NON_DIMENSION_PROPS, NON_BUBBLING_EVENTS } from '../constants'; 2 | import options from '../options'; 3 | import { toLowerCase, isString, isFunction, hashToClassName } from '../util'; 4 | 5 | 6 | 7 | 8 | /** Removes a given DOM Node from its parent. */ 9 | export function removeNode(node) { 10 | let p = node.parentNode; 11 | if (p) p.removeChild(node); 12 | } 13 | 14 | 15 | /** Set a named attribute on the given Node, with special behavior for some names and event handlers. 16 | * If `value` is `null`, the attribute/handler will be removed. 17 | * @param {Element} node An element to mutate 18 | * @param {string} name The name/key to set, such as an event or attribute name 19 | * @param {any} old The last value that was set for this name/node pair 20 | * @param {any} value An attribute value, such as a function to be used as an event handler 21 | * @param {Boolean} isSvg Are we currently diffing inside an svg? 22 | * @private 23 | */ 24 | export function setAccessor(node, name, old, value, isSvg) { 25 | 26 | if (name==='className') name = 'class'; 27 | 28 | if (name==='class' && value && typeof value==='object') { 29 | value = hashToClassName(value); 30 | } 31 | 32 | if (name==='key') { 33 | // ignore 34 | } 35 | else if (name==='class' && !isSvg) { 36 | node.className = value || ''; 37 | } 38 | else if (name==='style') { 39 | if (!value || isString(value) || isString(old)) { 40 | node.style.cssText = value || ''; 41 | } 42 | if (value && typeof value==='object') { 43 | if (!isString(old)) { 44 | for (let i in old) if (!(i in value)) node.style[i] = ''; 45 | } 46 | for (let i in value) { 47 | node.style[i] = typeof value[i]==='number' && !NON_DIMENSION_PROPS[i] ? (value[i]+'px') : value[i]; 48 | } 49 | } 50 | } 51 | else if (name==='dangerouslySetInnerHTML') { 52 | if (value) node.innerHTML = value.__html || ''; 53 | } 54 | else if (name[0]=='o' && name[1]=='n') { 55 | let l = node._listeners || (node._listeners = {}); 56 | name = toLowerCase(name.substring(2)); 57 | // @TODO: this might be worth it later, un-breaks focus/blur bubbling in IE9: 58 | // if (node.attachEvent) name = name=='focus'?'focusin':name=='blur'?'focusout':name; 59 | if (value) { 60 | if (!l[name]) node.addEventListener(name, eventProxy, !!NON_BUBBLING_EVENTS[name]); 61 | } 62 | else if (l[name]) { 63 | node.removeEventListener(name, eventProxy, !!NON_BUBBLING_EVENTS[name]); 64 | } 65 | l[name] = value; 66 | } 67 | else if (name!=='list' && name!=='type' && !isSvg && name in node) { 68 | setProperty(node, name, value==null ? '' : value); 69 | if (value==null || value===false) node.removeAttribute(name); 70 | } 71 | else { 72 | let ns = isSvg && name.match(/^xlink\:?(.+)/); 73 | if (value==null || value===false) { 74 | if (ns) node.removeAttributeNS('http://www.w3.org/1999/xlink', toLowerCase(ns[1])); 75 | else node.removeAttribute(name); 76 | } 77 | else if (typeof value!=='object' && !isFunction(value)) { 78 | if (ns) node.setAttributeNS('http://www.w3.org/1999/xlink', toLowerCase(ns[1]), value); 79 | else node.setAttribute(name, value); 80 | } 81 | } 82 | } 83 | 84 | 85 | /** Attempt to set a DOM property to the given value. 86 | * IE & FF throw for certain property-value combinations. 87 | */ 88 | function setProperty(node, name, value) { 89 | try { 90 | node[name] = value; 91 | } catch (e) { } 92 | } 93 | 94 | 95 | /** Proxy an event to hooked event handlers 96 | * @private 97 | */ 98 | function eventProxy(e) { 99 | return this._listeners[e.type](options.event && options.event(e) || e); 100 | } 101 | -------------------------------------------------------------------------------- /test/browser/spec.js: -------------------------------------------------------------------------------- 1 | import { h, render, rerender, Component } from '../../src/preact'; 2 | /** @jsx h */ 3 | 4 | const EMPTY_CHILDREN = []; 5 | 6 | describe('Component spec', () => { 7 | let scratch; 8 | 9 | before( () => { 10 | scratch = document.createElement('div'); 11 | (document.body || document.documentElement).appendChild(scratch); 12 | }); 13 | 14 | beforeEach( () => { 15 | scratch.innerHTML = ''; 16 | }); 17 | 18 | after( () => { 19 | scratch.parentNode.removeChild(scratch); 20 | scratch = null; 21 | }); 22 | 23 | describe('defaultProps', () => { 24 | it('should apply default props on initial render', () => { 25 | class WithDefaultProps extends Component { 26 | constructor(props, context) { 27 | super(props, context); 28 | expect(props).to.be.deep.equal({ 29 | children: EMPTY_CHILDREN, 30 | fieldA: 1, fieldB: 2, 31 | fieldC: 1, fieldD: 2 32 | }); 33 | } 34 | render() { 35 | return
    ; 36 | } 37 | } 38 | WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; 39 | render(, scratch); 40 | }); 41 | 42 | it('should apply default props on rerender', () => { 43 | let doRender; 44 | class Outer extends Component { 45 | constructor() { 46 | super(); 47 | this.state = { i:1 }; 48 | } 49 | componentDidMount() { 50 | doRender = () => this.setState({ i: 2 }); 51 | } 52 | render(props, { i }) { 53 | return ; 54 | } 55 | } 56 | class WithDefaultProps extends Component { 57 | constructor(props, context) { 58 | super(props, context); 59 | this.ctor(props, context); 60 | } 61 | ctor(){} 62 | componentWillReceiveProps() {} 63 | render() { 64 | return
    ; 65 | } 66 | } 67 | WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; 68 | 69 | let proto = WithDefaultProps.prototype; 70 | sinon.spy(proto, 'ctor'); 71 | sinon.spy(proto, 'componentWillReceiveProps'); 72 | sinon.spy(proto, 'render'); 73 | 74 | render(, scratch); 75 | doRender(); 76 | 77 | const PROPS1 = { 78 | fieldA: 1, fieldB: 1, 79 | fieldC: 1, fieldD: 1 80 | }; 81 | 82 | const PROPS2 = { 83 | fieldA: 1, fieldB: 2, 84 | fieldC: 1, fieldD: 2 85 | }; 86 | 87 | expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); 88 | expect(proto.render).to.have.been.calledWithMatch(PROPS1); 89 | 90 | rerender(); 91 | 92 | // expect(proto.ctor).to.have.been.calledWith(PROPS2); 93 | expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch(PROPS2); 94 | expect(proto.render).to.have.been.calledWithMatch(PROPS2); 95 | }); 96 | 97 | // @TODO: migrate this to preact-compat 98 | xit('should cache default props', () => { 99 | class WithDefaultProps extends Component { 100 | constructor(props, context) { 101 | super(props, context); 102 | expect(props).to.be.deep.equal({ 103 | fieldA: 1, fieldB: 2, 104 | fieldC: 1, fieldD: 2, 105 | fieldX: 10 106 | }); 107 | } 108 | getDefaultProps() { 109 | return { fieldA: 1, fieldB: 1 }; 110 | } 111 | render() { 112 | return
    ; 113 | } 114 | } 115 | WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; 116 | sinon.spy(WithDefaultProps.prototype, 'getDefaultProps'); 117 | render(( 118 |
    119 | 120 | 121 | 122 |
    123 | ), scratch); 124 | expect(WithDefaultProps.prototype.getDefaultProps).to.be.calledOnce; 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/browser/linked-state.js: -------------------------------------------------------------------------------- 1 | import { Component } from '../../src/preact'; 2 | import { createLinkedState } from '../../src/linked-state'; 3 | 4 | describe('linked-state', () => { 5 | class TestComponent extends Component { } 6 | let testComponent, linkFunction; 7 | 8 | before( () => { 9 | testComponent = new TestComponent(); 10 | sinon.spy(TestComponent.prototype, 'setState'); 11 | }); 12 | 13 | describe('createLinkedState without eventPath argument', () => { 14 | 15 | before( () => { 16 | linkFunction = createLinkedState(testComponent,'testStateKey'); 17 | expect(linkFunction).to.be.a('function'); 18 | }); 19 | 20 | beforeEach( () => { 21 | TestComponent.prototype['setState'].reset(); 22 | }); 23 | 24 | it('should use value attribute on text input when no eventPath is supplied', () => { 25 | let element = document.createElement('input'); 26 | element.type= 'text'; 27 | element.value = 'newValue'; 28 | 29 | linkFunction({ 30 | currentTarget: element, 31 | target: element 32 | }); 33 | 34 | expect(TestComponent.prototype.setState).to.have.been.calledOnce; 35 | expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'newValue'}); 36 | 37 | linkFunction.call(element); 38 | 39 | expect(TestComponent.prototype.setState).to.have.been.calledTwice; 40 | expect(TestComponent.prototype.setState.secondCall).to.have.been.calledWith({'testStateKey': 'newValue'}); 41 | }); 42 | 43 | it('should use checked attribute on checkbox input when no eventPath is supplied', () => { 44 | let checkboxElement = document.createElement('input'); 45 | checkboxElement.type= 'checkbox'; 46 | checkboxElement.checked = true; 47 | 48 | linkFunction({ 49 | currentTarget: checkboxElement, 50 | target: checkboxElement 51 | }); 52 | 53 | expect(TestComponent.prototype.setState).to.have.been.calledOnce; 54 | expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); 55 | }); 56 | 57 | it('should use checked attribute on radio input when no eventPath is supplied', () => { 58 | let radioElement = document.createElement('input'); 59 | radioElement.type= 'radio'; 60 | radioElement.checked = true; 61 | 62 | linkFunction({ 63 | currentTarget: radioElement, 64 | target: radioElement 65 | }); 66 | 67 | expect(TestComponent.prototype.setState).to.have.been.calledOnce; 68 | expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); 69 | }); 70 | 71 | 72 | it('should set dot notated state key appropriately', () => { 73 | linkFunction = createLinkedState(testComponent,'nested.state.key'); 74 | let element = document.createElement('input'); 75 | element.type= 'text'; 76 | element.value = 'newValue'; 77 | 78 | linkFunction({ 79 | currentTarget: element, 80 | target: element 81 | }); 82 | 83 | expect(TestComponent.prototype.setState).to.have.been.calledOnce; 84 | expect(TestComponent.prototype.setState).to.have.been.calledWith({nested: {state: {key: 'newValue'}}}); 85 | }); 86 | 87 | }); 88 | 89 | describe('createLinkedState with eventPath argument', () => { 90 | 91 | before( () => { 92 | linkFunction = createLinkedState(testComponent,'testStateKey', 'nested.path'); 93 | expect(linkFunction).to.be.a('function'); 94 | }); 95 | 96 | beforeEach( () => { 97 | TestComponent.prototype['setState'].reset(); 98 | }); 99 | 100 | it('should give precedence to nested.path on event over nested.path on component', () => { 101 | let event = {nested: {path: 'nestedPathValueFromEvent'}}; 102 | let component = {_component: {nested: {path: 'nestedPathValueFromComponent'}}}; 103 | 104 | linkFunction.call(component, event); 105 | 106 | expect(TestComponent.prototype.setState).to.have.been.calledOnce; 107 | expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'nestedPathValueFromEvent'}); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | import { FORCE_RENDER } from './constants'; 2 | import { extend, clone, isFunction } from './util'; 3 | import { createLinkedState } from './linked-state'; 4 | import { renderComponent } from './vdom/component'; 5 | import { enqueueRender } from './render-queue'; 6 | 7 | /** Base Component class, for the ES6 Class method of creating Components 8 | * @public 9 | * 10 | * @example 11 | * class MyFoo extends Component { 12 | * render(props, state) { 13 | * return
    ; 14 | * } 15 | * } 16 | */ 17 | export function Component(props, context) { 18 | /** @private */ 19 | this._dirty = true; 20 | // /** @public */ 21 | // this._disableRendering = false; 22 | // /** @public */ 23 | // this.prevState = this.prevProps = this.prevContext = this.base = this.nextBase = this._parentComponent = this._component = this.__ref = this.__key = this._linkedStates = this._renderCallbacks = null; 24 | /** @public */ 25 | this.context = context; 26 | /** @type {object} */ 27 | this.props = props; 28 | /** @type {object} */ 29 | if (!this.state) this.state = {}; 30 | } 31 | 32 | 33 | extend(Component.prototype, { 34 | 35 | /** Returns a `boolean` value indicating if the component should re-render when receiving the given `props` and `state`. 36 | * @param {object} nextProps 37 | * @param {object} nextState 38 | * @param {object} nextContext 39 | * @returns {Boolean} should the component re-render 40 | * @name shouldComponentUpdate 41 | * @function 42 | */ 43 | // shouldComponentUpdate() { 44 | // return true; 45 | // }, 46 | 47 | 48 | /** Returns a function that sets a state property when called. 49 | * Calling linkState() repeatedly with the same arguments returns a cached link function. 50 | * 51 | * Provides some built-in special cases: 52 | * - Checkboxes and radio buttons link their boolean `checked` value 53 | * - Inputs automatically link their `value` property 54 | * - Event paths fall back to any associated Component if not found on an element 55 | * - If linked value is a function, will invoke it and use the result 56 | * 57 | * @param {string} key The path to set - can be a dot-notated deep key 58 | * @param {string} [eventPath] If set, attempts to find the new state value at a given dot-notated path within the object passed to the linkedState setter. 59 | * @returns {function} linkStateSetter(e) 60 | * 61 | * @example Update a "text" state value when an input changes: 62 | * 63 | * 64 | * @example Set a deep state value on click 65 | * )/gi, (s, pre, attrs, after) => { 8 | let list = attrs.match(/\s[a-z0-9:_.-]+=".*?"/gi).sort( (a, b) => a>b ? 1 : -1 ); 9 | if (~after.indexOf('/')) after = '>'; 10 | return '<' + pre + list.join('') + after; 11 | }); 12 | } 13 | 14 | 15 | describe('svg', () => { 16 | let scratch; 17 | 18 | before( () => { 19 | scratch = document.createElement('div'); 20 | (document.body || document.documentElement).appendChild(scratch); 21 | }); 22 | 23 | beforeEach( () => { 24 | scratch.innerHTML = ''; 25 | }); 26 | 27 | after( () => { 28 | scratch.parentNode.removeChild(scratch); 29 | scratch = null; 30 | }); 31 | 32 | it('should render SVG to string', () => { 33 | render(( 34 | 35 | 36 | 37 | ), scratch); 38 | 39 | let html = sortAttributes(String(scratch.innerHTML).replace(' xmlns="http://www.w3.org/2000/svg"', '')); 40 | expect(html).to.equal(sortAttributes(` 41 | 42 | 43 | 44 | `.replace(/[\n\t]+/g,''))); 45 | }); 46 | 47 | it('should render SVG to DOM', () => { 48 | const Demo = () => ( 49 | 50 | 51 | 52 | ); 53 | render(, scratch); 54 | 55 | let html = sortAttributes(String(scratch.innerHTML).replace(' xmlns="http://www.w3.org/2000/svg"', '')); 56 | expect(html).to.equal(sortAttributes('')); 57 | }); 58 | 59 | it('should render with the correct namespace URI', () => { 60 | render(, scratch); 61 | 62 | let namespace = scratch.querySelector('svg').namespaceURI; 63 | 64 | expect(namespace).to.equal("http://www.w3.org/2000/svg"); 65 | }); 66 | 67 | it('should use attributes for className', () => { 68 | const Demo = ({ c }) => ( 69 | 70 | 71 | 72 | ); 73 | let root = render(, scratch, root); 74 | sinon.spy(root, 'removeAttribute'); 75 | root = render(, scratch, root); 76 | expect(root.removeAttribute).to.have.been.calledOnce.and.calledWith('class'); 77 | root.removeAttribute.restore(); 78 | 79 | root = render(
    , scratch, root); 80 | root = render(, scratch, root); 81 | sinon.spy(root, 'setAttribute'); 82 | root = render(, scratch, root); 83 | expect(root.setAttribute).to.have.been.calledOnce.and.calledWith('class', 'foo_2'); 84 | root.setAttribute.restore(); 85 | root = render(, scratch, root); 86 | root = render(, scratch, root); 87 | }); 88 | 89 | it('should still support class attribute', () => { 90 | render(( 91 | 92 | ), scratch); 93 | 94 | expect(scratch.innerHTML).to.contain(` class="foo bar"`); 95 | }); 96 | 97 | it('should serialize class', () => { 98 | render(( 99 | 100 | ), scratch); 101 | 102 | expect(scratch.innerHTML).to.contain(` class="foo other"`); 103 | }); 104 | 105 | it('should switch back to HTML for ', () => { 106 | render(( 107 | 108 | 109 | 110 | test 111 | 112 | 113 | 114 | ), scratch); 115 | 116 | expect(scratch.getElementsByTagName('a')) 117 | .to.have.property('0') 118 | .that.is.a('HTMLAnchorElement'); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact", 3 | "amdName": "preact", 4 | "version": "7.2.0", 5 | "description": "Tiny & fast Component-based virtual DOM framework.", 6 | "main": "dist/preact.js", 7 | "jsnext:main": "src/preact.js", 8 | "aliases:main": "aliases.js", 9 | "dev:main": "dist/preact.dev.js", 10 | "minified:main": "dist/preact.min.js", 11 | "scripts": { 12 | "clean": "rimraf dist/ aliases.js aliases.js.map devtools.js devtools.js.map", 13 | "copy-flow-definition": "copyfiles -f src/preact.js.flow dist", 14 | "copy-typescript-definition": "copyfiles -f src/preact.d.ts dist", 15 | "build": "npm-run-all --silent clean transpile copy-flow-definition copy-typescript-definition strip optimize minify size", 16 | "transpile:main": "rollup -c config/rollup.config.js -m dist/preact.dev.js.map -f umd -n preact src/preact.js -o dist/preact.dev.js", 17 | "transpile:devtools": "rollup -c config/rollup.config.devtools.js -o devtools.js -m devtools.js.map", 18 | "transpile:aliases": "rollup -c config/rollup.config.aliases.js -m aliases.js.map -f umd -n preact src/preact.js -o aliases.js", 19 | "transpile": "npm-run-all transpile:main transpile:aliases transpile:devtools", 20 | "optimize": "uglifyjs dist/preact.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty -b width=120,quote_style=3 -o dist/preact.js -p relative --in-source-map dist/preact.dev.js.map --source-map dist/preact.js.map", 21 | "minify": "uglifyjs dist/preact.js -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o dist/preact.min.js -p relative --in-source-map dist/preact.js.map --source-map dist/preact.min.js.map", 22 | "strip": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.dev.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.dev.js", 23 | "size": "node -e \"process.stdout.write('gzip size: ')\" && gzip-size dist/preact.min.js", 24 | "test": "npm-run-all lint --parallel test:mocha test:karma", 25 | "test:mocha": "mocha --recursive --compilers js:babel/register test/shared test/node", 26 | "test:karma": "karma start test/karma.conf.js --single-run", 27 | "test:mocha:watch": "npm run test:mocha -- --watch", 28 | "test:karma:watch": "npm run test:karma -- no-single-run", 29 | "lint": "eslint devtools src test", 30 | "prepublish": "npm run build", 31 | "smart-release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", 32 | "release": "cross-env npm run smart-release" 33 | }, 34 | "eslintConfig": { 35 | "extends": "./config/eslint-config.js" 36 | }, 37 | "typings": "./dist/preact.d.ts", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/developit/preact.git" 41 | }, 42 | "files": [ 43 | "devtools", 44 | "src", 45 | "dist", 46 | "aliases.js", 47 | "aliases.js.map", 48 | "devtools.js", 49 | "devtools.js.map", 50 | "typings.json" 51 | ], 52 | "keywords": [ 53 | "preact", 54 | "react", 55 | "virtual dom", 56 | "vdom", 57 | "components", 58 | "virtual", 59 | "dom" 60 | ], 61 | "author": "Jason Miller ", 62 | "license": "MIT", 63 | "bugs": { 64 | "url": "https://github.com/developit/preact/issues" 65 | }, 66 | "homepage": "https://github.com/developit/preact", 67 | "devDependencies": { 68 | "babel": "^5.8.23", 69 | "babel-core": "^5.8.24", 70 | "babel-eslint": "^6.1.0", 71 | "babel-loader": "^5.3.2", 72 | "babel-runtime": "^5.8.24", 73 | "chai": "^3.4.1", 74 | "copyfiles": "^1.0.0", 75 | "core-js": "^2.4.1", 76 | "coveralls": "^2.11.15", 77 | "cross-env": "^3.1.3", 78 | "diff": "^3.0.0", 79 | "eslint": "^3.0.0", 80 | "eslint-plugin-react": "^6.0.0", 81 | "gzip-size-cli": "^1.0.0", 82 | "isparta-loader": "^2.0.0", 83 | "jscodeshift": "^0.3.25", 84 | "karma": "^1.1.0", 85 | "karma-babel-preprocessor": "^5.2.2", 86 | "karma-chai": "^0.1.0", 87 | "karma-chai-sinon": "^0.1.5", 88 | "karma-chrome-launcher": "^2.0.0", 89 | "karma-coverage": "^1.0.0", 90 | "karma-mocha": "^1.1.1", 91 | "karma-mocha-reporter": "^2.0.4", 92 | "karma-phantomjs-launcher": "^1.0.1", 93 | "karma-sauce-launcher": "^1.1.0", 94 | "karma-source-map-support": "^1.1.0", 95 | "karma-sourcemap-loader": "^0.3.6", 96 | "karma-webpack": "^2.0.1", 97 | "mocha": "^3.0.1", 98 | "npm-run-all": "^4.0.0", 99 | "phantomjs-prebuilt": "^2.1.7", 100 | "rimraf": "^2.5.3", 101 | "rollup": "^0.40.0", 102 | "rollup-plugin-babel": "^1.0.0", 103 | "rollup-plugin-memory": "^2.0.0", 104 | "rollup-plugin-node-resolve": "^2.0.0", 105 | "sinon": "^1.17.4", 106 | "sinon-chai": "^2.8.0", 107 | "uglify-js": "^2.7.5", 108 | "webpack": "^1.13.1" 109 | }, 110 | "greenkeeper": { 111 | "ignore": [ 112 | "rollup-plugin-babel", 113 | "babel", 114 | "babel-core", 115 | "babel-eslint", 116 | "babel-loader", 117 | "babel-runtime", 118 | "jscodeshift" 119 | ] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/shared/h.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../src/preact'; 2 | import { VNode } from '../../src/vnode'; 3 | import { expect } from 'chai'; 4 | 5 | /*eslint-env browser, mocha */ 6 | 7 | /** @jsx h */ 8 | 9 | const buildVNode = (nodeName, attributes, children=[]) => ({ 10 | nodeName, 11 | children, 12 | attributes, 13 | key: attributes && attributes.key 14 | }); 15 | 16 | describe('h(jsx)', () => { 17 | it('should return a VNode', () => { 18 | let r; 19 | expect( () => r = h('foo') ).not.to.throw(); 20 | expect(r).to.be.an('object'); 21 | expect(r).to.be.an.instanceof(VNode); 22 | expect(r).to.have.property('nodeName', 'foo'); 23 | expect(r).to.have.property('attributes', undefined); 24 | expect(r).to.have.property('children').that.eql([]); 25 | }); 26 | 27 | it('should perserve raw attributes', () => { 28 | let attrs = { foo:'bar', baz:10, func:()=>{} }, 29 | r = h('foo', attrs); 30 | expect(r).to.be.an('object') 31 | .with.property('attributes') 32 | .that.deep.equals(attrs); 33 | }); 34 | 35 | it('should support element children', () => { 36 | let r = h( 37 | 'foo', 38 | null, 39 | h('bar'), 40 | h('baz') 41 | ); 42 | 43 | expect(r).to.be.an('object') 44 | .with.property('children') 45 | .that.deep.equals([ 46 | buildVNode('bar'), 47 | buildVNode('baz') 48 | ]); 49 | }); 50 | 51 | it('should support multiple element children, given as arg list', () => { 52 | let r = h( 53 | 'foo', 54 | null, 55 | h('bar'), 56 | h('baz', null, h('test')) 57 | ); 58 | 59 | expect(r).to.be.an('object') 60 | .with.property('children') 61 | .that.deep.equals([ 62 | buildVNode('bar'), 63 | buildVNode('baz', undefined, [ 64 | buildVNode('test') 65 | ]) 66 | ]); 67 | }); 68 | 69 | it('should handle multiple element children, given as an array', () => { 70 | let r = h( 71 | 'foo', 72 | null, 73 | [ 74 | h('bar'), 75 | h('baz', null, h('test')) 76 | ] 77 | ); 78 | 79 | expect(r).to.be.an('object') 80 | .with.property('children') 81 | .that.deep.equals([ 82 | buildVNode('bar'), 83 | buildVNode('baz', undefined, [ 84 | buildVNode('test') 85 | ]) 86 | ]); 87 | }); 88 | 89 | it('should handle multiple children, flattening one layer as needed', () => { 90 | let r = h( 91 | 'foo', 92 | null, 93 | h('bar'), 94 | [ 95 | h('baz', null, h('test')) 96 | ] 97 | ); 98 | 99 | expect(r).to.be.an('object') 100 | .with.property('children') 101 | .that.deep.equals([ 102 | buildVNode('bar'), 103 | buildVNode('baz', undefined, [ 104 | buildVNode('test') 105 | ]) 106 | ]); 107 | }); 108 | 109 | it('should support nested children', () => { 110 | const m = x => h(x); 111 | expect( 112 | h('foo', null, m('a'), [m('b'), m('c')], m('d')) 113 | ).to.have.property('children').that.eql(['a', 'b', 'c', 'd'].map(m)); 114 | 115 | expect( 116 | h('foo', null, [m('a'), [m('b'), m('c')], m('d')]) 117 | ).to.have.property('children').that.eql(['a', 'b', 'c', 'd'].map(m)); 118 | 119 | expect( 120 | h('foo', { children: [m('a'), [m('b'), m('c')], m('d')] }) 121 | ).to.have.property('children').that.eql(['a', 'b', 'c', 'd'].map(m)); 122 | 123 | expect( 124 | h('foo', { children: [[m('a'), [m('b'), m('c')], m('d')]] }) 125 | ).to.have.property('children').that.eql(['a', 'b', 'c', 'd'].map(m)); 126 | 127 | expect( 128 | h('foo', { children: m('a') }) 129 | ).to.have.property('children').that.eql([m('a')]); 130 | 131 | expect( 132 | h('foo', { children: 'a' }) 133 | ).to.have.property('children').that.eql(['a']); 134 | }); 135 | 136 | it('should support text children', () => { 137 | let r = h( 138 | 'foo', 139 | null, 140 | 'textstuff' 141 | ); 142 | 143 | expect(r).to.be.an('object') 144 | .with.property('children') 145 | .with.length(1) 146 | .with.property('0') 147 | .that.equals('textstuff'); 148 | }); 149 | 150 | it('should merge adjacent text children', () => { 151 | let r = h( 152 | 'foo', 153 | null, 154 | 'one', 155 | 'two', 156 | h('bar'), 157 | 'three', 158 | h('baz'), 159 | h('baz'), 160 | 'four', 161 | null, 162 | 'five', 163 | 'six' 164 | ); 165 | 166 | expect(r).to.be.an('object') 167 | .with.property('children') 168 | .that.deep.equals([ 169 | 'onetwo', 170 | buildVNode('bar'), 171 | 'three', 172 | buildVNode('baz'), 173 | buildVNode('baz'), 174 | 'fourfivesix' 175 | ]); 176 | }); 177 | 178 | it('should merge nested adjacent text children', () => { 179 | let r = h( 180 | 'foo', 181 | null, 182 | 'one', 183 | ['two', null, 'three'], 184 | null, 185 | ['four', null, 'five', null], 186 | 'six', 187 | null 188 | ); 189 | 190 | expect(r).to.be.an('object') 191 | .with.property('children') 192 | .that.deep.equals([ 193 | 'onetwothreefourfivesix' 194 | ]); 195 | }); 196 | it('should not merge children that are boolean values', () => { 197 | let r = h( 198 | 'foo', 199 | null, 200 | 'one', 201 | true, 202 | 'two', 203 | false, 204 | 'three' 205 | ); 206 | 207 | expect(r).to.be.an('object') 208 | .with.property('children') 209 | .that.deep.equals(['onetwothree']); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /test/browser/context.js: -------------------------------------------------------------------------------- 1 | import { h, render, Component } from '../../src/preact'; 2 | /** @jsx h */ 3 | 4 | const CHILDREN_MATCHER = sinon.match( v => v==null || Array.isArray(v) && !v.length , '[empty children]'); 5 | 6 | describe('context', () => { 7 | let scratch; 8 | 9 | before( () => { 10 | scratch = document.createElement('div'); 11 | (document.body || document.documentElement).appendChild(scratch); 12 | }); 13 | 14 | beforeEach( () => { 15 | scratch.innerHTML = ''; 16 | }); 17 | 18 | after( () => { 19 | scratch.parentNode.removeChild(scratch); 20 | scratch = null; 21 | }); 22 | 23 | it('should pass context to grandchildren', () => { 24 | const CONTEXT = { a:'a' }; 25 | const PROPS = { b:'b' }; 26 | // let inner; 27 | 28 | class Outer extends Component { 29 | getChildContext() { 30 | return CONTEXT; 31 | } 32 | render(props) { 33 | return
    ; 34 | } 35 | } 36 | sinon.spy(Outer.prototype, 'getChildContext'); 37 | 38 | class Inner extends Component { 39 | // constructor() { 40 | // super(); 41 | // inner = this; 42 | // } 43 | shouldComponentUpdate() { return true; } 44 | componentWillReceiveProps() {} 45 | componentWillUpdate() {} 46 | componentDidUpdate() {} 47 | render(props, state, context) { 48 | return
    { context && context.a }
    ; 49 | } 50 | } 51 | sinon.spy(Inner.prototype, 'shouldComponentUpdate'); 52 | sinon.spy(Inner.prototype, 'componentWillReceiveProps'); 53 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 54 | sinon.spy(Inner.prototype, 'componentDidUpdate'); 55 | sinon.spy(Inner.prototype, 'render'); 56 | 57 | render(, scratch, scratch.lastChild); 58 | 59 | expect(Outer.prototype.getChildContext).to.have.been.calledOnce; 60 | 61 | // initial render does not invoke anything but render(): 62 | expect(Inner.prototype.render).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}, CONTEXT); 63 | 64 | CONTEXT.foo = 'bar'; 65 | render(, scratch, scratch.lastChild); 66 | 67 | expect(Outer.prototype.getChildContext).to.have.been.calledTwice; 68 | 69 | let props = { children: CHILDREN_MATCHER, ...PROPS }; 70 | expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT); 71 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT); 72 | expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {}); 73 | expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}); 74 | expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT); 75 | 76 | 77 | /* Future: 78 | * Newly created context objects are *not* currently cloned. 79 | * This test checks that they *are* cloned. 80 | */ 81 | // Inner.prototype.render.reset(); 82 | // CONTEXT.foo = 'baz'; 83 | // inner.forceUpdate(); 84 | // expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, { a:'a', foo:'bar' }); 85 | }); 86 | 87 | it('should pass context to direct children', () => { 88 | const CONTEXT = { a:'a' }; 89 | const PROPS = { b:'b' }; 90 | 91 | class Outer extends Component { 92 | getChildContext() { 93 | return CONTEXT; 94 | } 95 | render(props) { 96 | return ; 97 | } 98 | } 99 | sinon.spy(Outer.prototype, 'getChildContext'); 100 | 101 | class Inner extends Component { 102 | shouldComponentUpdate() { return true; } 103 | componentWillReceiveProps() {} 104 | componentWillUpdate() {} 105 | componentDidUpdate() {} 106 | render(props, state, context) { 107 | return
    { context && context.a }
    ; 108 | } 109 | } 110 | sinon.spy(Inner.prototype, 'shouldComponentUpdate'); 111 | sinon.spy(Inner.prototype, 'componentWillReceiveProps'); 112 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 113 | sinon.spy(Inner.prototype, 'componentDidUpdate'); 114 | sinon.spy(Inner.prototype, 'render'); 115 | 116 | render(, scratch, scratch.lastChild); 117 | 118 | expect(Outer.prototype.getChildContext).to.have.been.calledOnce; 119 | 120 | // initial render does not invoke anything but render(): 121 | expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, CONTEXT); 122 | 123 | CONTEXT.foo = 'bar'; 124 | render(, scratch, scratch.lastChild); 125 | 126 | expect(Outer.prototype.getChildContext).to.have.been.calledTwice; 127 | 128 | let props = { children: CHILDREN_MATCHER, ...PROPS }; 129 | expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT); 130 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT); 131 | expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {}); 132 | expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}); 133 | expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT); 134 | 135 | // make sure render() could make use of context.a 136 | expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] })); 137 | }); 138 | 139 | it('should preserve existing context properties when creating child contexts', () => { 140 | let outerContext = { outer:true }, 141 | innerContext = { inner:true }; 142 | class Outer extends Component { 143 | getChildContext() { 144 | return { outerContext }; 145 | } 146 | render() { 147 | return
    ; 148 | } 149 | } 150 | 151 | class Inner extends Component { 152 | getChildContext() { 153 | return { innerContext }; 154 | } 155 | render() { 156 | return ; 157 | } 158 | } 159 | 160 | class InnerMost extends Component { 161 | render() { 162 | return test; 163 | } 164 | } 165 | 166 | sinon.spy(Inner.prototype, 'render'); 167 | sinon.spy(InnerMost.prototype, 'render'); 168 | 169 | render(, scratch); 170 | 171 | expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext }); 172 | expect(InnerMost.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext, innerContext }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/browser/performance.js: -------------------------------------------------------------------------------- 1 | /*global coverage, ENABLE_PERFORMANCE, NODE_ENV*/ 2 | /*eslint no-console:0*/ 3 | /** @jsx h */ 4 | 5 | let { h, Component, render } = require(NODE_ENV==='production' ? '../../dist/preact.min.js' : '../../src/preact'); 6 | 7 | const MULTIPLIER = ENABLE_PERFORMANCE ? (coverage ? 5 : 1) : 999999; 8 | 9 | 10 | let now = typeof performance!=='undefined' && performance.now ? () => performance.now() : () => +new Date(); 11 | 12 | function loop(iter, time) { 13 | let start = now(), 14 | count = 0; 15 | while ( now()-start < time ) { 16 | count++; 17 | iter(); 18 | } 19 | return count; 20 | } 21 | 22 | 23 | function benchmark(iter, callback) { 24 | let a = 0; // eslint-disable-line no-unused-vars 25 | function noop() { 26 | try { a++; } finally { a += Math.random(); } 27 | } 28 | 29 | // warm 30 | for (let i=3; i--; ) noop(), iter(); 31 | 32 | let count = 5, 33 | time = 200, 34 | passes = 0, 35 | noops = loop(noop, time), 36 | iterations = 0; 37 | 38 | function next() { 39 | iterations += loop(iter, time); 40 | setTimeout(++passes===count ? done : next, 10); 41 | } 42 | 43 | function done() { 44 | let ticks = Math.round(noops / iterations * count), 45 | hz = iterations / count / time * 1000, 46 | message = `${hz|0}/s (${ticks} ticks)`; 47 | callback({ iterations, noops, count, time, ticks, hz, message }); 48 | } 49 | 50 | next(); 51 | } 52 | 53 | 54 | describe('performance', function() { 55 | let scratch; 56 | 57 | this.timeout(10000); 58 | 59 | before( () => { 60 | if (coverage) { 61 | console.warn('WARNING: Code coverage is enabled, which dramatically reduces performance. Do not pay attention to these numbers.'); 62 | } 63 | scratch = document.createElement('div'); 64 | (document.body || document.documentElement).appendChild(scratch); 65 | }); 66 | 67 | beforeEach( () => { 68 | scratch.innerHTML = ''; 69 | }); 70 | 71 | after( () => { 72 | scratch.parentNode.removeChild(scratch); 73 | scratch = null; 74 | }); 75 | 76 | it('should rerender without changes fast', done => { 77 | let jsx = ( 78 |
    79 |
    80 |

    a {'b'} c {0} d

    81 | 85 |
    86 |
    87 |
    {}}> 88 | 89 | 90 |
    91 | 92 | 93 |
    94 | 95 | 96 | 97 | 98 | 99 | 100 |
    101 |
    102 |
    103 | ); 104 | 105 | let root; 106 | benchmark( () => { 107 | root = render(jsx, scratch, root); 108 | }, ({ ticks, message }) => { 109 | console.log(`PERF: empty diff: ${message}`); 110 | expect(ticks).to.be.below(350 * MULTIPLIER); 111 | done(); 112 | }); 113 | }); 114 | 115 | it('should rerender repeated trees fast', done => { 116 | class Header extends Component { 117 | render() { 118 | return ( 119 |
    120 |

    a {'b'} c {0} d

    121 | 125 |
    126 | ); 127 | } 128 | } 129 | class Form extends Component { 130 | render() { 131 | return ( 132 |
    {}}> 133 | 134 | 135 |
    136 | 137 | 138 |
    139 | 140 | 141 | ); 142 | } 143 | } 144 | class ButtonBar extends Component { 145 | render() { 146 | return ( 147 | 148 | 149 | 150 | 151 | 152 | 153 | ); 154 | } 155 | } 156 | class Button extends Component { 157 | render(props) { 158 | return 231 | 232 | 233 | 234 | 235 | 236 | 237 |
    238 | ); 239 | }, ({ ticks, message }) => { 240 | console.log(`PERF: large VTree: ${message}`); 241 | expect(ticks).to.be.below(2000 * MULTIPLIER); 242 | done(); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /test/browser/devtools.js: -------------------------------------------------------------------------------- 1 | import { h, Component, render } from '../../src/preact'; 2 | import { initDevTools } from '../../devtools/devtools'; 3 | import { unmountComponent } from '../../src/vdom/component'; 4 | 5 | class StatefulComponent extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = {count: 0}; 10 | } 11 | 12 | render() { 13 | return h('span', {}, String(this.state.count)); 14 | } 15 | } 16 | 17 | function FunctionalComponent() { 18 | return h('span', {class: 'functional'}, 'Functional'); 19 | } 20 | 21 | function Label({label}) { 22 | return label; 23 | } 24 | 25 | class MultiChild extends Component { 26 | constructor(props) { 27 | super(props); 28 | this.state = {count: props.initialCount}; 29 | } 30 | 31 | render() { 32 | return h('div', {}, Array(this.state.count).fill('child')); 33 | } 34 | } 35 | 36 | let describe_ = describe; 37 | if (!('name' in Function.prototype)) { 38 | // Skip these tests under Internet Explorer 39 | describe_ = describe.skip; 40 | } 41 | 42 | describe_('React Developer Tools integration', () => { 43 | let cleanup; 44 | let container; 45 | let renderer; 46 | 47 | // Maps of DOM node to React*Component-like objects. 48 | // For composite components, there will be two instances for each node, one 49 | // for the composite component (instanceMap) and one for the root child DOM 50 | // component rendered by that component (domInstanceMap) 51 | let instanceMap = new Map(); 52 | let domInstanceMap = new Map(); 53 | 54 | beforeEach(() => { 55 | container = document.createElement('div'); 56 | document.body.appendChild(container); 57 | 58 | const onMount = instance => { 59 | if (instance._renderedChildren) { 60 | domInstanceMap.set(instance.node, instance); 61 | } else { 62 | instanceMap.set(instance.node, instance); 63 | } 64 | }; 65 | 66 | const onUnmount = instance => { 67 | instanceMap.delete(instance.node); 68 | domInstanceMap.delete(instance.node); 69 | }; 70 | 71 | global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { 72 | inject: sinon.spy(_renderer => { 73 | renderer = _renderer; 74 | renderer.Mount._renderNewRootComponent = sinon.stub(); 75 | renderer.Reconciler.mountComponent = sinon.spy(onMount); 76 | renderer.Reconciler.unmountComponent = sinon.spy(onUnmount); 77 | renderer.Reconciler.receiveComponent = sinon.stub(); 78 | }) 79 | }; 80 | cleanup = initDevTools(); 81 | }); 82 | 83 | afterEach(() => { 84 | container.remove(); 85 | cleanup(); 86 | }); 87 | 88 | it('registers preact as a renderer with the React DevTools hook', () => { 89 | expect(global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject).to.be.called; 90 | }); 91 | 92 | // Basic component addition/update/removal tests 93 | it('notifies dev tools about new components', () => { 94 | render(h(StatefulComponent), container); 95 | expect(renderer.Reconciler.mountComponent).to.be.called; 96 | }); 97 | 98 | it('notifies dev tools about component updates', () => { 99 | const node = render(h(StatefulComponent), container); 100 | node._component.forceUpdate(); 101 | expect(renderer.Reconciler.receiveComponent).to.be.called; 102 | }); 103 | 104 | it('notifies dev tools when components are removed', () => { 105 | const node = render(h(StatefulComponent), container); 106 | unmountComponent(node._component, true); 107 | expect(renderer.Reconciler.unmountComponent).to.be.called; 108 | }); 109 | 110 | // Test properties of DOM components exposed to devtools via 111 | // ReactDOMComponent-like instances 112 | it('exposes the tag name of DOM components', () => { 113 | const node = render(h(StatefulComponent), container); 114 | const domInstance = domInstanceMap.get(node); 115 | expect(domInstance._currentElement.type).to.equal('span'); 116 | }); 117 | 118 | it('exposes DOM component props', () => { 119 | const node = render(h(FunctionalComponent), container); 120 | const domInstance = domInstanceMap.get(node); 121 | expect(domInstance._currentElement.props.class).to.equal('functional'); 122 | }); 123 | 124 | it('exposes text component contents', () => { 125 | const node = render(h(Label, {label: 'Text content'}), container); 126 | const textInstance = domInstanceMap.get(node); 127 | expect(textInstance._stringText).to.equal('Text content'); 128 | }); 129 | 130 | // Test properties of composite components exposed to devtools via 131 | // ReactCompositeComponent-like instances 132 | it('exposes the name of composite component classes', () => { 133 | const node = render(h(StatefulComponent), container); 134 | expect(instanceMap.get(node).getName()).to.equal('StatefulComponent'); 135 | }); 136 | 137 | it('exposes composite component props', () => { 138 | const node = render(h(Label, {label: 'Text content'}), container); 139 | const instance = instanceMap.get(node); 140 | expect(instance._currentElement.props.label).to.equal('Text content'); 141 | }); 142 | 143 | it('exposes composite component state', () => { 144 | const node = render(h(StatefulComponent), container); 145 | 146 | node._component.setState({count: 42}); 147 | node._component.forceUpdate(); 148 | 149 | expect(instanceMap.get(node).state).to.deep.equal({count: 42}); 150 | }); 151 | 152 | // Test setting state via devtools 153 | it('updates component when setting state from devtools', () => { 154 | const node = render(h(StatefulComponent), container); 155 | 156 | instanceMap.get(node).setState({count: 10}); 157 | instanceMap.get(node).forceUpdate(); 158 | 159 | expect(node.textContent).to.equal('10'); 160 | }); 161 | 162 | // Test that the original instance is exposed via `_instance` so it can 163 | // be accessed conveniently via `$r` in devtools 164 | 165 | // Functional component handling tests 166 | it('wraps functional components with stateful ones', () => { 167 | const vnode = h(FunctionalComponent); 168 | expect(vnode.nodeName.prototype).to.have.property('render'); 169 | }); 170 | 171 | it('exposes the name of functional components', () => { 172 | const node = render(h(FunctionalComponent), container); 173 | const instance = instanceMap.get(node); 174 | expect(instance.getName()).to.equal('FunctionalComponent'); 175 | }); 176 | 177 | it('exposes a fallback name if the component has no useful name', () => { 178 | const node = render(h(() => h('div')), container); 179 | const instance = instanceMap.get(node); 180 | expect(instance.getName()).to.equal('(Function.name missing)'); 181 | }); 182 | 183 | // Test handling of DOM children 184 | it('notifies dev tools about DOM children', () => { 185 | const node = render(h(StatefulComponent), container); 186 | const domInstance = domInstanceMap.get(node); 187 | expect(renderer.Reconciler.mountComponent).to.have.been.calledWith(domInstance); 188 | }); 189 | 190 | it('notifies dev tools when a component update adds DOM children', () => { 191 | const node = render(h(MultiChild, {initialCount: 2}), container); 192 | 193 | node._component.setState({count: 4}); 194 | node._component.forceUpdate(); 195 | 196 | expect(renderer.Reconciler.mountComponent).to.have.been.called.twice; 197 | }); 198 | 199 | it('notifies dev tools when a component update modifies DOM children', () => { 200 | const node = render(h(StatefulComponent), container); 201 | 202 | instanceMap.get(node).setState({count: 10}); 203 | instanceMap.get(node).forceUpdate(); 204 | 205 | const textInstance = domInstanceMap.get(node.childNodes[0]); 206 | expect(textInstance._stringText).to.equal('10'); 207 | }); 208 | 209 | it('notifies dev tools when a component update removes DOM children', () => { 210 | const node = render(h(MultiChild, {initialCount: 1}), container); 211 | 212 | node._component.setState({count: 0}); 213 | node._component.forceUpdate(); 214 | 215 | expect(renderer.Reconciler.unmountComponent).to.be.called; 216 | }); 217 | 218 | // Root component info 219 | it('exposes root components on the _instancesByReactRootID map', () => { 220 | render(h(StatefulComponent), container); 221 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); 222 | }); 223 | 224 | it('notifies dev tools when new root components are mounted', () => { 225 | render(h(StatefulComponent), container); 226 | expect(renderer.Mount._renderNewRootComponent).to.be.called; 227 | }); 228 | 229 | it('removes root components when they are unmounted', () => { 230 | const node = render(h(StatefulComponent), container); 231 | unmountComponent(node._component, true); 232 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(0); 233 | }); 234 | 235 | it('counts root components correctly when a root renders a composite child', () => { 236 | function Child() { 237 | return h('main'); 238 | } 239 | function Parent() { 240 | return h(Child); 241 | } 242 | 243 | render(h(Parent), container); 244 | 245 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); 246 | }); 247 | 248 | it('counts root components correctly when a native element has a composite child', () => { 249 | function Link() { 250 | return h('a'); 251 | } 252 | function Root() { 253 | return h('div', {}, h(Link)); 254 | } 255 | 256 | render(h(Root), container); 257 | 258 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /test/browser/refs.js: -------------------------------------------------------------------------------- 1 | import { h, render, Component } from '../../src/preact'; 2 | /** @jsx h */ 3 | 4 | // gives call count and argument errors names (otherwise sinon just uses "spy"): 5 | let spy = (name, ...args) => { 6 | let spy = sinon.spy(...args); 7 | spy.displayName = `spy('${name}')`; 8 | return spy; 9 | }; 10 | 11 | describe('refs', () => { 12 | let scratch; 13 | 14 | before( () => { 15 | scratch = document.createElement('div'); 16 | (document.body || document.documentElement).appendChild(scratch); 17 | }); 18 | 19 | beforeEach( () => { 20 | scratch.innerHTML = ''; 21 | }); 22 | 23 | after( () => { 24 | scratch.parentNode.removeChild(scratch); 25 | scratch = null; 26 | }); 27 | 28 | it('should invoke refs in render()', () => { 29 | let ref = spy('ref'); 30 | render(
    , scratch); 31 | expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild); 32 | }); 33 | 34 | it('should invoke refs in Component.render()', () => { 35 | let outer = spy('outer'), 36 | inner = spy('inner'); 37 | class Foo extends Component { 38 | render() { 39 | return ( 40 |
    41 | 42 |
    43 | ); 44 | } 45 | } 46 | render(, scratch); 47 | 48 | expect(outer).to.have.been.calledWith(scratch.firstChild); 49 | expect(inner).to.have.been.calledWith(scratch.firstChild.firstChild); 50 | }); 51 | 52 | it('should pass components to ref functions', () => { 53 | let ref = spy('ref'), 54 | instance; 55 | class Foo extends Component { 56 | constructor() { 57 | super(); 58 | instance = this; 59 | } 60 | render() { 61 | return
    ; 62 | } 63 | } 64 | render(, scratch); 65 | 66 | expect(ref).to.have.been.calledOnce.and.calledWith(instance); 67 | }); 68 | 69 | it('should pass rendered DOM from functional components to ref functions', () => { 70 | let ref = spy('ref'); 71 | 72 | const Foo = () =>
    ; 73 | 74 | let root = render(, scratch); 75 | expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild); 76 | 77 | ref.reset(); 78 | render(, scratch, root); 79 | expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild); 80 | 81 | ref.reset(); 82 | render(, scratch, root); 83 | expect(ref).to.have.been.calledOnce.and.calledWith(null); 84 | }); 85 | 86 | it('should pass children to ref functions', () => { 87 | let outer = spy('outer'), 88 | inner = spy('inner'), 89 | rerender, inst; 90 | class Outer extends Component { 91 | constructor() { 92 | super(); 93 | rerender = () => this.forceUpdate(); 94 | } 95 | render() { 96 | return ( 97 |
    98 | 99 |
    100 | ); 101 | } 102 | } 103 | class Inner extends Component { 104 | constructor() { 105 | super(); 106 | inst = this; 107 | } 108 | render() { 109 | return ; 110 | } 111 | } 112 | 113 | let root = render(, scratch); 114 | 115 | expect(outer).to.have.been.calledOnce.and.calledWith(inst); 116 | expect(inner).to.have.been.calledOnce.and.calledWith(inst.base); 117 | 118 | outer.reset(); 119 | inner.reset(); 120 | 121 | rerender(); 122 | 123 | expect(outer).to.have.been.calledOnce.and.calledWith(inst); 124 | expect(inner).to.have.been.calledOnce.and.calledWith(inst.base); 125 | 126 | outer.reset(); 127 | inner.reset(); 128 | 129 | render(
    , scratch, root); 130 | 131 | expect(outer).to.have.been.calledOnce.and.calledWith(null); 132 | expect(inner).to.have.been.calledOnce.and.calledWith(null); 133 | }); 134 | 135 | it('should pass high-order children to ref functions', () => { 136 | let outer = spy('outer'), 137 | inner = spy('inner'), 138 | innermost = spy('innermost'), 139 | outerInst, 140 | innerInst; 141 | class Outer extends Component { 142 | constructor() { 143 | super(); 144 | outerInst = this; 145 | } 146 | render() { 147 | return ; 148 | } 149 | } 150 | class Inner extends Component { 151 | constructor() { 152 | super(); 153 | innerInst = this; 154 | } 155 | render() { 156 | return ; 157 | } 158 | } 159 | 160 | let root = render(, scratch); 161 | 162 | expect(outer, 'outer initial').to.have.been.calledOnce.and.calledWith(outerInst); 163 | expect(inner, 'inner initial').to.have.been.calledOnce.and.calledWith(innerInst); 164 | expect(innermost, 'innerMost initial').to.have.been.calledOnce.and.calledWith(innerInst.base); 165 | 166 | outer.reset(); 167 | inner.reset(); 168 | innermost.reset(); 169 | root = render(, scratch, root); 170 | 171 | expect(outer, 'outer update').to.have.been.calledOnce.and.calledWith(outerInst); 172 | expect(inner, 'inner update').to.have.been.calledOnce.and.calledWith(innerInst); 173 | expect(innermost, 'innerMost update').to.have.been.calledOnce.and.calledWith(innerInst.base); 174 | 175 | outer.reset(); 176 | inner.reset(); 177 | innermost.reset(); 178 | render(
    , scratch, root); 179 | 180 | expect(outer, 'outer unmount').to.have.been.calledOnce.and.calledWith(null); 181 | expect(inner, 'inner unmount').to.have.been.calledOnce.and.calledWith(null); 182 | expect(innermost, 'innerMost unmount').to.have.been.calledOnce.and.calledWith(null); 183 | }); 184 | 185 | it('should not pass ref into component as a prop', () => { 186 | let foo = spy('foo'), 187 | bar = spy('bar'); 188 | 189 | class Foo extends Component { 190 | render(){ return
    ; } 191 | } 192 | const Bar = spy('Bar', () =>
    ); 193 | 194 | sinon.spy(Foo.prototype, 'render'); 195 | 196 | render(( 197 |
    198 | 199 | 200 |
    201 | ), scratch); 202 | 203 | expect(Foo.prototype.render).to.have.been.calledWithMatch({ ref:sinon.match.falsy, a:'a' }, { }, { }); 204 | expect(Bar).to.have.been.calledWithMatch({ b:'b', ref:bar }, { }); 205 | }); 206 | 207 | // Test for #232 208 | it('should only null refs after unmount', () => { 209 | let root, outer, inner; 210 | 211 | class TestUnmount extends Component { 212 | componentWillUnmount() { 213 | expect(this).to.have.property('outer', outer); 214 | expect(this).to.have.property('inner', inner); 215 | } 216 | 217 | componentDidUnmount() { 218 | expect(this).to.have.property('outer', null); 219 | expect(this).to.have.property('inner', null); 220 | } 221 | 222 | render() { 223 | return ( 224 |
    this.outer=c }> 225 |
    this.inner=c } /> 226 |
    227 | ); 228 | } 229 | } 230 | 231 | sinon.spy(TestUnmount.prototype, 'componentWillUnmount'); 232 | sinon.spy(TestUnmount.prototype, 'componentDidUnmount'); 233 | 234 | root = render(
    , scratch, root); 235 | outer = scratch.querySelector('#outer'); 236 | inner = scratch.querySelector('#inner'); 237 | 238 | expect(TestUnmount.prototype.componentWillUnmount).not.to.have.been.called; 239 | expect(TestUnmount.prototype.componentDidUnmount).not.to.have.been.called; 240 | 241 | render(
    , scratch, root); 242 | 243 | expect(TestUnmount.prototype.componentWillUnmount).to.have.been.calledOnce; 244 | expect(TestUnmount.prototype.componentDidUnmount).to.have.been.calledOnce; 245 | }); 246 | 247 | it('should null and re-invoke refs when swapping component root element type', () => { 248 | let inst; 249 | 250 | class App extends Component { 251 | render() { 252 | return
    ; 253 | } 254 | } 255 | 256 | class Child extends Component { 257 | constructor(props, context) { 258 | super(props, context); 259 | this.state = { show:false }; 260 | inst = this; 261 | } 262 | handleMount(){} 263 | render(_, { show }) { 264 | if (!show) return
    ; 265 | return some test content; 266 | } 267 | } 268 | sinon.spy(Child.prototype, 'handleMount'); 269 | 270 | render(, scratch); 271 | expect(inst.handleMount).to.have.been.calledOnce.and.calledWith(scratch.querySelector('#div')); 272 | inst.handleMount.reset(); 273 | 274 | inst.setState({ show:true }); 275 | inst.forceUpdate(); 276 | expect(inst.handleMount).to.have.been.calledTwice; 277 | expect(inst.handleMount.firstCall).to.have.been.calledWith(null); 278 | expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#span')); 279 | inst.handleMount.reset(); 280 | 281 | inst.setState({ show:false }); 282 | inst.forceUpdate(); 283 | expect(inst.handleMount).to.have.been.calledTwice; 284 | expect(inst.handleMount.firstCall).to.have.been.calledWith(null); 285 | expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#div')); 286 | }); 287 | 288 | 289 | it('should add refs to components representing DOM nodes with no attributes if they have been pre-rendered', () => { 290 | // Simulate pre-render 291 | let parent = document.createElement('div'); 292 | let child = document.createElement('div'); 293 | parent.appendChild(child); 294 | scratch.appendChild(parent); // scratch contains:
    295 | 296 | let ref = spy('ref'); 297 | 298 | function Wrapper() { 299 | return
    ; 300 | } 301 | 302 | render(
    , scratch, scratch.firstChild); 303 | expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild.firstChild); 304 | }); 305 | }); 306 | -------------------------------------------------------------------------------- /src/vdom/component.js: -------------------------------------------------------------------------------- 1 | import { SYNC_RENDER, NO_RENDER, FORCE_RENDER, ASYNC_RENDER, ATTR_KEY } from '../constants'; 2 | import options from '../options'; 3 | import { isFunction, clone, extend } from '../util'; 4 | import { enqueueRender } from '../render-queue'; 5 | import { getNodeProps } from './index'; 6 | import { diff, mounts, diffLevel, flushMounts, recollectNodeTree } from './diff'; 7 | import { isFunctionalComponent, buildFunctionalComponent } from './functional-component'; 8 | import { createComponent, collectComponent } from './component-recycler'; 9 | import { removeNode } from '../dom/index'; 10 | 11 | 12 | 13 | /** Set a component's `props` (generally derived from JSX attributes). 14 | * @param {Object} props 15 | * @param {Object} [opts] 16 | * @param {boolean} [opts.renderSync=false] If `true` and {@link options.syncComponentUpdates} is `true`, triggers synchronous rendering. 17 | * @param {boolean} [opts.render=true] If `false`, no render will be triggered. 18 | */ 19 | export function setComponentProps(component, props, opts, context, mountAll) { 20 | if (component._disable) return; 21 | component._disable = true; 22 | 23 | if ((component.__ref = props.ref)) delete props.ref; 24 | if ((component.__key = props.key)) delete props.key; 25 | 26 | if (!component.base || mountAll) { 27 | if (component.componentWillMount) component.componentWillMount(); 28 | } 29 | else if (component.componentWillReceiveProps) { 30 | component.componentWillReceiveProps(props, context); 31 | } 32 | 33 | if (context && context!==component.context) { 34 | if (!component.prevContext) component.prevContext = component.context; 35 | component.context = context; 36 | } 37 | 38 | if (!component.prevProps) component.prevProps = component.props; 39 | component.props = props; 40 | 41 | component._disable = false; 42 | 43 | if (opts!==NO_RENDER) { 44 | if (opts===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) { 45 | renderComponent(component, SYNC_RENDER, mountAll); 46 | } 47 | else { 48 | enqueueRender(component); 49 | } 50 | } 51 | 52 | if (component.__ref) component.__ref(component); 53 | } 54 | 55 | 56 | 57 | /** Render a Component, triggering necessary lifecycle events and taking High-Order Components into account. 58 | * @param {Component} component 59 | * @param {Object} [opts] 60 | * @param {boolean} [opts.build=false] If `true`, component will build and store a DOM node if not already associated with one. 61 | * @private 62 | */ 63 | export function renderComponent(component, opts, mountAll, isChild) { 64 | if (component._disable) return; 65 | 66 | let skip, rendered, 67 | props = component.props, 68 | state = component.state, 69 | context = component.context, 70 | previousProps = component.prevProps || props, 71 | previousState = component.prevState || state, 72 | previousContext = component.prevContext || context, 73 | isUpdate = component.base, 74 | nextBase = component.nextBase, 75 | initialBase = isUpdate || nextBase, 76 | initialChildComponent = component._component, 77 | inst, cbase; 78 | 79 | // if updating 80 | if (isUpdate) { 81 | component.props = previousProps; 82 | component.state = previousState; 83 | component.context = previousContext; 84 | if (opts!==FORCE_RENDER 85 | && component.shouldComponentUpdate 86 | && component.shouldComponentUpdate(props, state, context) === false) { 87 | skip = true; 88 | } 89 | else if (component.componentWillUpdate) { 90 | component.componentWillUpdate(props, state, context); 91 | } 92 | component.props = props; 93 | component.state = state; 94 | component.context = context; 95 | } 96 | 97 | component.prevProps = component.prevState = component.prevContext = component.nextBase = null; 98 | component._dirty = false; 99 | 100 | if (!skip) { 101 | if (component.render) rendered = component.render(props, state, context); 102 | 103 | // context to pass to the child, can be updated via (grand-)parent component 104 | if (component.getChildContext) { 105 | context = extend(clone(context), component.getChildContext()); 106 | } 107 | 108 | while (isFunctionalComponent(rendered)) { 109 | rendered = buildFunctionalComponent(rendered, context); 110 | } 111 | 112 | let childComponent = rendered && rendered.nodeName, 113 | toUnmount, base; 114 | 115 | if (isFunction(childComponent)) { 116 | // set up high order component link 117 | 118 | let childProps = getNodeProps(rendered); 119 | inst = initialChildComponent; 120 | 121 | if (inst && inst.constructor===childComponent && childProps.key==inst.__key) { 122 | setComponentProps(inst, childProps, SYNC_RENDER, context); 123 | } 124 | else { 125 | toUnmount = inst; 126 | 127 | inst = createComponent(childComponent, childProps, context); 128 | inst.nextBase = inst.nextBase || nextBase; 129 | inst._parentComponent = component; 130 | component._component = inst; 131 | setComponentProps(inst, childProps, NO_RENDER, context); 132 | renderComponent(inst, SYNC_RENDER, mountAll, true); 133 | } 134 | 135 | base = inst.base; 136 | } 137 | else { 138 | cbase = initialBase; 139 | 140 | // destroy high order component link 141 | toUnmount = initialChildComponent; 142 | if (toUnmount) { 143 | cbase = component._component = null; 144 | } 145 | 146 | if (initialBase || opts===SYNC_RENDER) { 147 | if (cbase) cbase._component = null; 148 | base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true); 149 | } 150 | } 151 | 152 | if (initialBase && base!==initialBase && inst!==initialChildComponent) { 153 | let baseParent = initialBase.parentNode; 154 | if (baseParent && base!==baseParent) { 155 | baseParent.replaceChild(base, initialBase); 156 | 157 | if (!toUnmount) { 158 | initialBase._component = null; 159 | recollectNodeTree(initialBase); 160 | } 161 | } 162 | } 163 | 164 | if (toUnmount) { 165 | unmountComponent(toUnmount, base!==initialBase); 166 | } 167 | 168 | component.base = base; 169 | if (base && !isChild) { 170 | let componentRef = component, 171 | t = component; 172 | while ((t=t._parentComponent)) { 173 | (componentRef = t).base = base; 174 | } 175 | base._component = componentRef; 176 | base._componentConstructor = componentRef.constructor; 177 | } 178 | } 179 | 180 | if (!isUpdate || mountAll) { 181 | mounts.unshift(component); 182 | } 183 | else if (!skip) { 184 | if (component.componentDidUpdate) { 185 | component.componentDidUpdate(previousProps, previousState, previousContext); 186 | } 187 | if (options.afterUpdate) options.afterUpdate(component); 188 | } 189 | 190 | let cb = component._renderCallbacks, fn; 191 | if (cb) while ( (fn = cb.pop()) ) fn.call(component); 192 | 193 | if (!diffLevel && !isChild) flushMounts(); 194 | } 195 | 196 | 197 | 198 | /** Apply the Component referenced by a VNode to the DOM. 199 | * @param {Element} dom The DOM node to mutate 200 | * @param {VNode} vnode A Component-referencing VNode 201 | * @returns {Element} dom The created/mutated element 202 | * @private 203 | */ 204 | export function buildComponentFromVNode(dom, vnode, context, mountAll) { 205 | let c = dom && dom._component, 206 | originalComponent = c, 207 | oldDom = dom, 208 | isDirectOwner = c && dom._componentConstructor===vnode.nodeName, 209 | isOwner = isDirectOwner, 210 | props = getNodeProps(vnode); 211 | while (c && !isOwner && (c=c._parentComponent)) { 212 | isOwner = c.constructor===vnode.nodeName; 213 | } 214 | 215 | if (c && isOwner && (!mountAll || c._component)) { 216 | setComponentProps(c, props, ASYNC_RENDER, context, mountAll); 217 | dom = c.base; 218 | } 219 | else { 220 | if (originalComponent && !isDirectOwner) { 221 | unmountComponent(originalComponent, true); 222 | dom = oldDom = null; 223 | } 224 | 225 | c = createComponent(vnode.nodeName, props, context); 226 | if (dom && !c.nextBase) { 227 | c.nextBase = dom; 228 | // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L241: 229 | oldDom = null; 230 | } 231 | setComponentProps(c, props, SYNC_RENDER, context, mountAll); 232 | dom = c.base; 233 | 234 | if (oldDom && dom!==oldDom) { 235 | oldDom._component = null; 236 | recollectNodeTree(oldDom); 237 | } 238 | } 239 | 240 | return dom; 241 | } 242 | 243 | 244 | 245 | /** Remove a component from the DOM and recycle it. 246 | * @param {Element} dom A DOM node from which to unmount the given Component 247 | * @param {Component} component The Component instance to unmount 248 | * @private 249 | */ 250 | export function unmountComponent(component, remove) { 251 | if (options.beforeUnmount) options.beforeUnmount(component); 252 | 253 | // console.log(`${remove?'Removing':'Unmounting'} component: ${component.constructor.name}`); 254 | let base = component.base; 255 | 256 | component._disable = true; 257 | 258 | if (component.componentWillUnmount) component.componentWillUnmount(); 259 | 260 | component.base = null; 261 | 262 | // recursively tear down & recollect high-order component children: 263 | let inner = component._component; 264 | if (inner) { 265 | unmountComponent(inner, remove); 266 | } 267 | else if (base) { 268 | if (base[ATTR_KEY] && base[ATTR_KEY].ref) base[ATTR_KEY].ref(null); 269 | 270 | component.nextBase = base; 271 | 272 | if (remove) { 273 | removeNode(base); 274 | collectComponent(component); 275 | } 276 | let c; 277 | while ((c=base.lastChild)) recollectNodeTree(c, !remove); 278 | // removeOrphanedChildren(base.childNodes, true); 279 | } 280 | 281 | if (component.__ref) component.__ref(null); 282 | if (component.componentDidUnmount) component.componentDidUnmount(); 283 | } 284 | -------------------------------------------------------------------------------- /src/vdom/diff.js: -------------------------------------------------------------------------------- 1 | import { ATTR_KEY } from '../constants'; 2 | import { isString, isFunction } from '../util'; 3 | import { isSameNodeType, isNamedNode } from './index'; 4 | import { isFunctionalComponent, buildFunctionalComponent } from './functional-component'; 5 | import { buildComponentFromVNode } from './component'; 6 | import { setAccessor, removeNode } from '../dom/index'; 7 | import { createNode, collectNode } from '../dom/recycler'; 8 | import { unmountComponent } from './component'; 9 | import options from '../options'; 10 | 11 | 12 | /** Queue of components that have been mounted and are awaiting componentDidMount */ 13 | export const mounts = []; 14 | 15 | /** Diff recursion count, used to track the end of the diff cycle. */ 16 | export let diffLevel = 0; 17 | 18 | /** Global flag indicating if the diff is currently within an SVG */ 19 | let isSvgMode = false; 20 | 21 | /** Global flag indicating if the diff is performing hydration */ 22 | let hydrating = false; 23 | 24 | /** Invoke queued componentDidMount lifecycle methods */ 25 | export function flushMounts() { 26 | let c; 27 | while ((c=mounts.pop())) { 28 | if (options.afterMount) options.afterMount(c); 29 | if (c.componentDidMount) c.componentDidMount(); 30 | } 31 | } 32 | 33 | 34 | /** Apply differences in a given vnode (and it's deep children) to a real DOM Node. 35 | * @param {Element} [dom=null] A DOM node to mutate into the shape of the `vnode` 36 | * @param {VNode} vnode A VNode (with descendants forming a tree) representing the desired DOM structure 37 | * @returns {Element} dom The created/mutated element 38 | * @private 39 | */ 40 | export function diff(dom, vnode, context, mountAll, parent, componentRoot) { 41 | // diffLevel having been 0 here indicates initial entry into the diff (not a subdiff) 42 | if (!diffLevel++) { 43 | // when first starting the diff, check if we're diffing an SVG or within an SVG 44 | isSvgMode = parent && typeof parent.ownerSVGElement!=='undefined'; 45 | 46 | // hydration is inidicated by the existing element to be diffed not having a prop cache 47 | hydrating = dom && !(ATTR_KEY in dom); 48 | } 49 | 50 | let ret = idiff(dom, vnode, context, mountAll); 51 | 52 | // append the element if its a new parent 53 | if (parent && ret.parentNode!==parent) parent.appendChild(ret); 54 | 55 | // diffLevel being reduced to 0 means we're exiting the diff 56 | if (!--diffLevel) { 57 | hydrating = false; 58 | // invoke queued componentDidMount lifecycle methods 59 | if (!componentRoot) flushMounts(); 60 | } 61 | 62 | return ret; 63 | } 64 | 65 | 66 | function idiff(dom, vnode, context, mountAll) { 67 | let ref = vnode && vnode.attributes && vnode.attributes.ref; 68 | 69 | 70 | // Resolve ephemeral Pure Functional Components 71 | while (isFunctionalComponent(vnode)) { 72 | vnode = buildFunctionalComponent(vnode, context); 73 | } 74 | 75 | 76 | // empty values (null & undefined) render as empty Text nodes 77 | if (vnode==null) vnode = ''; 78 | 79 | 80 | // Fast case: Strings create/update Text nodes. 81 | if (isString(vnode)) { 82 | // update if it's already a Text node 83 | if (dom && dom instanceof Text && dom.parentNode) { 84 | if (dom.nodeValue!=vnode) { 85 | dom.nodeValue = vnode; 86 | } 87 | } 88 | else { 89 | // it wasn't a Text node: replace it with one and recycle the old Element 90 | if (dom) recollectNodeTree(dom); 91 | dom = document.createTextNode(vnode); 92 | } 93 | 94 | return dom; 95 | } 96 | 97 | 98 | // If the VNode represents a Component, perform a component diff. 99 | if (isFunction(vnode.nodeName)) { 100 | return buildComponentFromVNode(dom, vnode, context, mountAll); 101 | } 102 | 103 | 104 | let out = dom, 105 | nodeName = String(vnode.nodeName), // @TODO this masks undefined component errors as `` 106 | prevSvgMode = isSvgMode, 107 | vchildren = vnode.children; 108 | 109 | 110 | // SVGs have special namespace stuff. 111 | // This tracks entering and exiting that namespace when descending through the tree. 112 | isSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode; 113 | 114 | 115 | if (!dom) { 116 | // case: we had no element to begin with 117 | // - create an element with the nodeName from VNode 118 | out = createNode(nodeName, isSvgMode); 119 | } 120 | else if (!isNamedNode(dom, nodeName)) { 121 | // case: Element and VNode had different nodeNames 122 | // - need to create the correct Element to match VNode 123 | // - then migrate children from old to new 124 | 125 | out = createNode(nodeName, isSvgMode); 126 | 127 | // move children into the replacement node 128 | while (dom.firstChild) out.appendChild(dom.firstChild); 129 | 130 | // if the previous Element was mounted into the DOM, replace it inline 131 | if (dom.parentNode) dom.parentNode.replaceChild(out, dom); 132 | 133 | // recycle the old element (skips non-Element node types) 134 | recollectNodeTree(dom); 135 | } 136 | 137 | 138 | let fc = out.firstChild, 139 | props = out[ATTR_KEY]; 140 | 141 | // Attribute Hydration: if there is no prop cache on the element, 142 | // ...create it and populate it with the element's attributes. 143 | if (!props) { 144 | out[ATTR_KEY] = props = {}; 145 | for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value; 146 | } 147 | 148 | // Optimization: fast-path for elements containing a single TextNode: 149 | if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc && fc instanceof Text && !fc.nextSibling) { 150 | if (fc.nodeValue!=vchildren[0]) { 151 | fc.nodeValue = vchildren[0]; 152 | } 153 | } 154 | // otherwise, if there are existing or new children, diff them: 155 | else if (vchildren && vchildren.length || fc) { 156 | innerDiffNode(out, vchildren, context, mountAll, !!props.dangerouslySetInnerHTML); 157 | } 158 | 159 | 160 | // Apply attributes/props from VNode to the DOM Element: 161 | diffAttributes(out, vnode.attributes, props); 162 | 163 | 164 | // invoke original ref (from before resolving Pure Functional Components): 165 | if (ref) { 166 | (props.ref = ref)(out); 167 | } 168 | 169 | isSvgMode = prevSvgMode; 170 | 171 | return out; 172 | } 173 | 174 | 175 | /** Apply child and attribute changes between a VNode and a DOM Node to the DOM. 176 | * @param {Element} dom Element whose children should be compared & mutated 177 | * @param {Array} vchildren Array of VNodes to compare to `dom.childNodes` 178 | * @param {Object} context Implicitly descendant context object (from most recent `getChildContext()`) 179 | * @param {Boolean} mountAll 180 | * @param {Boolean} absorb If `true`, consumes externally created elements similar to hydration 181 | */ 182 | function innerDiffNode(dom, vchildren, context, mountAll, absorb) { 183 | let originalChildren = dom.childNodes, 184 | children = [], 185 | keyed = {}, 186 | keyedLen = 0, 187 | min = 0, 188 | len = originalChildren.length, 189 | childrenLen = 0, 190 | vlen = vchildren && vchildren.length, 191 | j, c, vchild, child; 192 | 193 | if (len) { 194 | for (let i=0; i=len) { 245 | dom.appendChild(child); 246 | } 247 | else if (child!==originalChildren[i]) { 248 | if (child===originalChildren[i+1]) { 249 | removeNode(originalChildren[i]); 250 | } 251 | dom.insertBefore(child, originalChildren[i] || null); 252 | } 253 | } 254 | } 255 | } 256 | 257 | 258 | if (keyedLen) { 259 | for (let i in keyed) if (keyed[i]) recollectNodeTree(keyed[i]); 260 | } 261 | 262 | // remove orphaned children 263 | while (min<=childrenLen) { 264 | child = children[childrenLen--]; 265 | if (child) recollectNodeTree(child); 266 | } 267 | } 268 | 269 | 270 | 271 | /** Recursively recycle (or just unmount) a node an its descendants. 272 | * @param {Node} node DOM node to start unmount/removal from 273 | * @param {Boolean} [unmountOnly=false] If `true`, only triggers unmount lifecycle, skips removal 274 | */ 275 | export function recollectNodeTree(node, unmountOnly) { 276 | let component = node._component; 277 | if (component) { 278 | // if node is owned by a Component, unmount that component (ends up recursing back here) 279 | unmountComponent(component, !unmountOnly); 280 | } 281 | else { 282 | // If the node's VNode had a ref function, invoke it with null here. 283 | // (this is part of the React spec, and smart for unsetting references) 284 | if (node[ATTR_KEY] && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null); 285 | 286 | if (!unmountOnly) { 287 | collectNode(node); 288 | } 289 | 290 | // Recollect/unmount all children. 291 | // - we use .lastChild here because it causes less reflow than .firstChild 292 | // - it's also cheaper than accessing the .childNodes Live NodeList 293 | let c; 294 | while ((c=node.lastChild)) recollectNodeTree(c, unmountOnly); 295 | } 296 | } 297 | 298 | 299 | 300 | /** Apply differences in attributes from a VNode to the given DOM Element. 301 | * @param {Element} dom Element with attributes to diff `attrs` against 302 | * @param {Object} attrs The desired end-state key-value attribute pairs 303 | * @param {Object} old Current/previous attributes (from previous VNode or element's prop cache) 304 | */ 305 | function diffAttributes(dom, attrs, old) { 306 | // remove attributes no longer present on the vnode by setting them to undefined 307 | let name; 308 | for (name in old) { 309 | if (!(attrs && name in attrs) && old[name]!=null) { 310 | setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode); 311 | } 312 | } 313 | 314 | // add new & update changed attributes 315 | if (attrs) { 316 | for (name in attrs) { 317 | if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) { 318 | setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode); 319 | } 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /devtools/devtools.js: -------------------------------------------------------------------------------- 1 | /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ 2 | 3 | import { options, Component } from 'preact'; 4 | 5 | // Internal helpers from preact 6 | import { ATTR_KEY } from '../src/constants'; 7 | import { isFunctionalComponent } from '../src/vdom/functional-component'; 8 | 9 | /** 10 | * Return a ReactElement-compatible object for the current state of a preact 11 | * component. 12 | */ 13 | function createReactElement(component) { 14 | return { 15 | type: component.constructor, 16 | key: component.key, 17 | ref: null, // Unsupported 18 | props: component.props 19 | }; 20 | } 21 | 22 | /** 23 | * Create a ReactDOMComponent-compatible object for a given DOM node rendered 24 | * by preact. 25 | * 26 | * This implements the subset of the ReactDOMComponent interface that 27 | * React DevTools requires in order to display DOM nodes in the inspector with 28 | * the correct type and properties. 29 | * 30 | * @param {Node} node 31 | */ 32 | function createReactDOMComponent(node) { 33 | const childNodes = node.nodeType === Node.ELEMENT_NODE ? 34 | Array.from(node.childNodes) : []; 35 | 36 | const isText = node.nodeType === Node.TEXT_NODE; 37 | 38 | return { 39 | // --- ReactDOMComponent interface 40 | _currentElement: isText ? node.textContent : { 41 | type: node.nodeName.toLowerCase(), 42 | props: node[ATTR_KEY] 43 | }, 44 | _renderedChildren: childNodes.map(child => { 45 | if (child._component) { 46 | return updateReactComponent(child._component); 47 | } 48 | return updateReactComponent(child); 49 | }), 50 | _stringText: isText ? node.textContent : null, 51 | 52 | // --- Additional properties used by preact devtools 53 | 54 | // A flag indicating whether the devtools have been notified about the 55 | // existence of this component instance yet. 56 | // This is used to send the appropriate notifications when DOM components 57 | // are added or updated between composite component updates. 58 | _inDevTools: false, 59 | node 60 | }; 61 | } 62 | 63 | /** 64 | * Return the name of a component created by a `ReactElement`-like object. 65 | * 66 | * @param {ReactElement} element 67 | */ 68 | function typeName(element) { 69 | if (typeof element.type === 'function') { 70 | return element.type.displayName || element.type.name; 71 | } 72 | return element.type; 73 | } 74 | 75 | /** 76 | * Return a ReactCompositeComponent-compatible object for a given preact 77 | * component instance. 78 | * 79 | * This implements the subset of the ReactCompositeComponent interface that 80 | * the DevTools requires in order to walk the component tree and inspect the 81 | * component's properties. 82 | * 83 | * See https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/getData.js 84 | */ 85 | function createReactCompositeComponent(component) { 86 | const _currentElement = createReactElement(component); 87 | const node = component.base; 88 | 89 | let instance = { 90 | // --- ReactDOMComponent properties 91 | getName() { 92 | return typeName(_currentElement); 93 | }, 94 | _currentElement: createReactElement(component), 95 | props: component.props, 96 | state: component.state, 97 | forceUpdate: component.forceUpdate.bind(component), 98 | setState: component.setState.bind(component), 99 | 100 | // --- Additional properties used by preact devtools 101 | node 102 | }; 103 | 104 | // React DevTools exposes the `_instance` field of the selected item in the 105 | // component tree as `$r` in the console. `_instance` must refer to a 106 | // React Component (or compatible) class instance with `props` and `state` 107 | // fields and `setState()`, `forceUpdate()` methods. 108 | instance._instance = component; 109 | 110 | // If the root node returned by this component instance's render function 111 | // was itself a composite component, there will be a `_component` property 112 | // containing the child component instance. 113 | if (component._component) { 114 | instance._renderedComponent = updateReactComponent(component._component); 115 | } else { 116 | // Otherwise, if the render() function returned an HTML/SVG element, 117 | // create a ReactDOMComponent-like object for the DOM node itself. 118 | instance._renderedComponent = updateReactComponent(node); 119 | } 120 | 121 | return instance; 122 | } 123 | 124 | /** 125 | * Map of Component|Node to ReactDOMComponent|ReactCompositeComponent-like 126 | * object. 127 | * 128 | * The same React*Component instance must be used when notifying devtools 129 | * about the initial mount of a component and subsequent updates. 130 | */ 131 | let instanceMap = new Map(); 132 | 133 | /** 134 | * Update (and create if necessary) the ReactDOMComponent|ReactCompositeComponent-like 135 | * instance for a given preact component instance or DOM Node. 136 | * 137 | * @param {Component|Node} componentOrNode 138 | */ 139 | function updateReactComponent(componentOrNode) { 140 | const newInstance = componentOrNode instanceof Node ? 141 | createReactDOMComponent(componentOrNode) : 142 | createReactCompositeComponent(componentOrNode); 143 | if (instanceMap.has(componentOrNode)) { 144 | let inst = instanceMap.get(componentOrNode); 145 | Object.assign(inst, newInstance); 146 | return inst; 147 | } 148 | instanceMap.set(componentOrNode, newInstance); 149 | return newInstance; 150 | } 151 | 152 | function nextRootKey(roots) { 153 | return '.' + Object.keys(roots).length; 154 | } 155 | 156 | /** 157 | * Find all root component instances rendered by preact in `node`'s children 158 | * and add them to the `roots` map. 159 | * 160 | * @param {DOMElement} node 161 | * @param {[key: string] => ReactDOMComponent|ReactCompositeComponent} 162 | */ 163 | function findRoots(node, roots) { 164 | Array.from(node.childNodes).forEach(child => { 165 | if (child._component) { 166 | roots[nextRootKey(roots)] = updateReactComponent(child._component); 167 | } else { 168 | findRoots(child, roots); 169 | } 170 | }); 171 | } 172 | 173 | /** 174 | * Map of functional component name -> wrapper class. 175 | */ 176 | let functionalComponentWrappers = new Map(); 177 | 178 | /** 179 | * Wrap a functional component with a stateful component. 180 | * 181 | * preact does not record any information about the original hierarchy of 182 | * functional components in the rendered DOM nodes. Wrapping functional components 183 | * with a trivial wrapper allows us to recover information about the original 184 | * component structure from the DOM. 185 | * 186 | * @param {VNode} vnode 187 | */ 188 | function wrapFunctionalComponent(vnode) { 189 | const originalRender = vnode.nodeName; 190 | const name = vnode.nodeName.name || '(Function.name missing)'; 191 | const wrappers = functionalComponentWrappers; 192 | if (!wrappers.has(originalRender)) { 193 | let wrapper = class extends Component { 194 | render(props, state, context) { 195 | return originalRender(props, context); 196 | } 197 | }; 198 | 199 | // Expose the original component name. React Dev Tools will use 200 | // this property if it exists or fall back to Function.name 201 | // otherwise. 202 | wrapper.displayName = name; 203 | 204 | wrappers.set(originalRender, wrapper); 205 | } 206 | vnode.nodeName = wrappers.get(originalRender); 207 | } 208 | 209 | /** 210 | * Create a bridge for exposing preact's component tree to React DevTools. 211 | * 212 | * It creates implementations of the interfaces that ReactDOM passes to 213 | * devtools to enable it to query the component tree and hook into component 214 | * updates. 215 | * 216 | * See https://github.com/facebook/react/blob/59ff7749eda0cd858d5ee568315bcba1be75a1ca/src/renderers/dom/ReactDOM.js 217 | * for how ReactDOM exports its internals for use by the devtools and 218 | * the `attachRenderer()` function in 219 | * https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/attachRenderer.js 220 | * for how the devtools consumes the resulting objects. 221 | */ 222 | function createDevToolsBridge() { 223 | // The devtools has different paths for interacting with the renderers from 224 | // React Native, legacy React DOM and current React DOM. 225 | // 226 | // Here we emulate the interface for the current React DOM (v15+) lib. 227 | 228 | // ReactDOMComponentTree-like object 229 | const ComponentTree = { 230 | getNodeFromInstance(instance) { 231 | return instance.node; 232 | }, 233 | getClosestInstanceFromNode(node) { 234 | while (node && !node._component) { 235 | node = node.parentNode; 236 | } 237 | return node ? updateReactComponent(node._component) : null; 238 | } 239 | }; 240 | 241 | // Map of root ID (the ID is unimportant) to component instance. 242 | let roots = {}; 243 | findRoots(document.body, roots); 244 | 245 | // ReactMount-like object 246 | // 247 | // Used by devtools to discover the list of root component instances and get 248 | // notified when new root components are rendered. 249 | const Mount = { 250 | _instancesByReactRootID: roots, 251 | 252 | // Stub - React DevTools expects to find this method and replace it 253 | // with a wrapper in order to observe new root components being added 254 | _renderNewRootComponent(/* instance, ... */) { } 255 | }; 256 | 257 | // ReactReconciler-like object 258 | const Reconciler = { 259 | // Stubs - React DevTools expects to find these methods and replace them 260 | // with wrappers in order to observe components being mounted, updated and 261 | // unmounted 262 | mountComponent(/* instance, ... */) { }, 263 | performUpdateIfNecessary(/* instance, ... */) { }, 264 | receiveComponent(/* instance, ... */) { }, 265 | unmountComponent(/* instance, ... */) { } 266 | }; 267 | 268 | /** Notify devtools that a new component instance has been mounted into the DOM. */ 269 | const componentAdded = component => { 270 | const instance = updateReactComponent(component); 271 | if (isRootComponent(component)) { 272 | instance._rootID = nextRootKey(roots); 273 | roots[instance._rootID] = instance; 274 | Mount._renderNewRootComponent(instance); 275 | } 276 | visitNonCompositeChildren(instance, childInst => { 277 | childInst._inDevTools = true; 278 | Reconciler.mountComponent(childInst); 279 | }); 280 | Reconciler.mountComponent(instance); 281 | }; 282 | 283 | /** Notify devtools that a component has been updated with new props/state. */ 284 | const componentUpdated = component => { 285 | const prevRenderedChildren = []; 286 | visitNonCompositeChildren(instanceMap.get(component), childInst => { 287 | prevRenderedChildren.push(childInst); 288 | }); 289 | 290 | // Notify devtools about updates to this component and any non-composite 291 | // children 292 | const instance = updateReactComponent(component); 293 | Reconciler.receiveComponent(instance); 294 | visitNonCompositeChildren(instance, childInst => { 295 | if (!childInst._inDevTools) { 296 | // New DOM child component 297 | childInst._inDevTools = true; 298 | Reconciler.mountComponent(childInst); 299 | } else { 300 | // Updated DOM child component 301 | Reconciler.receiveComponent(childInst); 302 | } 303 | }); 304 | 305 | // For any non-composite children that were removed by the latest render, 306 | // remove the corresponding ReactDOMComponent-like instances and notify 307 | // the devtools 308 | prevRenderedChildren.forEach(childInst => { 309 | if (!document.body.contains(childInst.node)) { 310 | instanceMap.delete(childInst.node); 311 | Reconciler.unmountComponent(childInst); 312 | } 313 | }); 314 | }; 315 | 316 | /** Notify devtools that a component has been unmounted from the DOM. */ 317 | const componentRemoved = component => { 318 | const instance = updateReactComponent(component); 319 | visitNonCompositeChildren(childInst => { 320 | instanceMap.delete(childInst.node); 321 | Reconciler.unmountComponent(childInst); 322 | }); 323 | Reconciler.unmountComponent(instance); 324 | instanceMap.delete(component); 325 | if (instance._rootID) { 326 | delete roots[instance._rootID]; 327 | } 328 | }; 329 | 330 | return { 331 | componentAdded, 332 | componentUpdated, 333 | componentRemoved, 334 | 335 | // Interfaces passed to devtools via __REACT_DEVTOOLS_GLOBAL_HOOK__.inject() 336 | ComponentTree, 337 | Mount, 338 | Reconciler 339 | }; 340 | } 341 | 342 | /** 343 | * Return `true` if a preact component is a top level component rendered by 344 | * `render()` into a container Element. 345 | */ 346 | function isRootComponent(component) { 347 | if (component._parentComponent) { 348 | // Component with a composite parent 349 | return false; 350 | } 351 | if (component.base.parentElement && component.base.parentElement[ATTR_KEY]) { 352 | // Component with a parent DOM element rendered by Preact 353 | return false; 354 | } 355 | return true; 356 | } 357 | 358 | /** 359 | * Visit all child instances of a ReactCompositeComponent-like object that are 360 | * not composite components (ie. they represent DOM elements or text) 361 | * 362 | * @param {Component} component 363 | * @param {(Component) => void} visitor 364 | */ 365 | function visitNonCompositeChildren(component, visitor) { 366 | if (component._renderedComponent) { 367 | if (!component._renderedComponent._component) { 368 | visitor(component._renderedComponent); 369 | visitNonCompositeChildren(component._renderedComponent, visitor); 370 | } 371 | } else if (component._renderedChildren) { 372 | component._renderedChildren.forEach(child => { 373 | visitor(child); 374 | if (!child._component) visitNonCompositeChildren(child, visitor); 375 | }); 376 | } 377 | } 378 | 379 | /** 380 | * Create a bridge between the preact component tree and React's dev tools 381 | * and register it. 382 | * 383 | * After this function is called, the React Dev Tools should be able to detect 384 | * "React" on the page and show the component tree. 385 | * 386 | * This function hooks into preact VNode creation in order to expose functional 387 | * components correctly, so it should be called before the root component(s) 388 | * are rendered. 389 | * 390 | * Returns a cleanup function which unregisters the hooks. 391 | */ 392 | export function initDevTools() { 393 | if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') { 394 | // React DevTools are not installed 395 | return; 396 | } 397 | 398 | // Hook into preact element creation in order to wrap functional components 399 | // with stateful ones in order to make them visible in the devtools 400 | const nextVNode = options.vnode; 401 | options.vnode = (vnode) => { 402 | if (isFunctionalComponent(vnode)) wrapFunctionalComponent(vnode); 403 | if (nextVNode) return nextVNode(vnode); 404 | }; 405 | 406 | // Notify devtools when preact components are mounted, updated or unmounted 407 | const bridge = createDevToolsBridge(); 408 | 409 | const nextAfterMount = options.afterMount; 410 | options.afterMount = component => { 411 | bridge.componentAdded(component); 412 | if (nextAfterMount) nextAfterMount(component); 413 | }; 414 | 415 | const nextAfterUpdate = options.afterUpdate; 416 | options.afterUpdate = component => { 417 | bridge.componentUpdated(component); 418 | if (nextAfterUpdate) nextAfterUpdate(component); 419 | }; 420 | 421 | const nextBeforeUnmount = options.beforeUnmount; 422 | options.beforeUnmount = component => { 423 | bridge.componentRemoved(component); 424 | if (nextBeforeUnmount) nextBeforeUnmount(component); 425 | }; 426 | 427 | // Notify devtools about this instance of "React" 428 | __REACT_DEVTOOLS_GLOBAL_HOOK__.inject(bridge); 429 | 430 | return () => { 431 | options.afterMount = nextAfterMount; 432 | options.afterUpdate = nextAfterUpdate; 433 | options.beforeUnmount = nextBeforeUnmount; 434 | }; 435 | } 436 | -------------------------------------------------------------------------------- /src/preact.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace preact { 2 | interface ComponentProps { 3 | children?:JSX.Element[]; 4 | key?:string | number | any; 5 | } 6 | 7 | interface DangerouslySetInnerHTML { 8 | __html: string; 9 | } 10 | 11 | interface PreactHTMLAttributes { 12 | dangerouslySetInnerHTML?:DangerouslySetInnerHTML; 13 | key?:string; 14 | ref?:(el?: Element) => void; 15 | } 16 | 17 | interface VNode { 18 | nodeName:ComponentConstructor|string; 19 | attributes:{[name:string]:any}; 20 | children:VNode[]; 21 | key:string; 22 | } 23 | 24 | interface ComponentLifecycle { 25 | componentWillMount?():void; 26 | componentDidMount?():void; 27 | componentWillUnmount?():void; 28 | componentDidUnmount?():void; 29 | componentWillReceiveProps?(nextProps:PropsType,nextContext:any):void; 30 | shouldComponentUpdate?(nextProps:PropsType,nextState:StateType,nextContext:any):boolean; 31 | componentWillUpdate?(nextProps:PropsType,nextState:StateType,nextContext:any):void; 32 | componentDidUpdate?(previousProps:PropsType,previousState:StateType,previousContext:any):void; 33 | } 34 | 35 | interface ComponentConstructor { 36 | new (props?:PropsType):Component; 37 | } 38 | 39 | abstract class Component implements ComponentLifecycle { 40 | constructor(props?:PropsType); 41 | 42 | state:StateType; 43 | props:PropsType & ComponentProps; 44 | base:HTMLElement; 45 | 46 | linkState:(name:string) => (event: Event) => void; 47 | 48 | setState(state:Pick, callback?:() => void):void; 49 | setState(fn:(prevState:StateType, props:PropsType) => Pick, callback?:() => void):void; 50 | 51 | forceUpdate(): void; 52 | 53 | abstract render(props:PropsType & ComponentProps, state:any):JSX.Element; 54 | } 55 | 56 | function h(node:ComponentConstructor, params:PropsType, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element; 57 | function h(node:string, params:JSX.HTMLAttributes&JSX.SVGAttributes&{[propName: string]: any}, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element; 58 | function render(node:JSX.Element, parent:Element, mergeWith?:Element):Element; 59 | function rerender():void; 60 | function cloneElement(element:JSX.Element, props:any):JSX.Element; 61 | 62 | var options:{ 63 | syncComponentUpdates?:boolean; 64 | debounceRendering?:(render:() => void) => void; 65 | vnode?:(vnode:VNode) => void; 66 | event?:(event:Event) => Event; 67 | }; 68 | } 69 | 70 | declare module "preact" { 71 | export = preact; 72 | } 73 | 74 | declare module "preact/devtools" { 75 | // Empty. This module initializes the React Developer Tools integration 76 | // when imported. 77 | } 78 | 79 | declare namespace JSX { 80 | interface Element extends preact.VNode { 81 | } 82 | 83 | interface ElementClass extends preact.Component { 84 | } 85 | 86 | interface ElementAttributesProperty { 87 | props:any; 88 | } 89 | 90 | interface SVGAttributes { 91 | clipPath?:string; 92 | cx?:number | string; 93 | cy?:number | string; 94 | d?:string; 95 | dx?:number | string; 96 | dy?:number | string; 97 | fill?:string; 98 | fillOpacity?:number | string; 99 | fontFamily?:string; 100 | fontSize?:number | string; 101 | fx?:number | string; 102 | fy?:number | string; 103 | gradientTransform?:string; 104 | gradientUnits?:string; 105 | markerEnd?:string; 106 | markerMid?:string; 107 | markerStart?:string; 108 | offset?:number | string; 109 | opacity?:number | string; 110 | patternContentUnits?:string; 111 | patternUnits?:string; 112 | points?:string; 113 | preserveAspectRatio?:string; 114 | r?:number | string; 115 | rx?:number | string; 116 | ry?:number | string; 117 | spreadMethod?:string; 118 | stopColor?:string; 119 | stopOpacity?:number | string; 120 | stroke?:string; 121 | strokeDasharray?:string; 122 | strokeLinecap?:string; 123 | strokeMiterlimit?:string; 124 | strokeOpacity?:number | string; 125 | strokeWidth?:number | string; 126 | textAnchor?:string; 127 | transform?:string; 128 | version?:string; 129 | viewBox?:string; 130 | x1?:number | string; 131 | x2?:number | string; 132 | x?:number | string; 133 | xlinkActuate?:string; 134 | xlinkArcrole?:string; 135 | xlinkHref?:string; 136 | xlinkRole?:string; 137 | xlinkShow?:string; 138 | xlinkTitle?:string; 139 | xlinkType?:string; 140 | xmlBase?:string; 141 | xmlLang?:string; 142 | xmlSpace?:string; 143 | y1?:number | string; 144 | y2?:number | string; 145 | y?:number | string; 146 | } 147 | 148 | interface PathAttributes { 149 | d:string; 150 | } 151 | 152 | interface EventHandler { 153 | (event:E):void; 154 | } 155 | 156 | type ClipboardEventHandler = EventHandler; 157 | type CompositionEventHandler = EventHandler; 158 | type DragEventHandler = EventHandler; 159 | type FocusEventHandler = EventHandler; 160 | type KeyboardEventHandler = EventHandler; 161 | type MouseEventHandler = EventHandler; 162 | type TouchEventHandler = EventHandler; 163 | type UIEventHandler = EventHandler; 164 | type WheelEventHandler = EventHandler; 165 | type AnimationEventHandler = EventHandler; 166 | type TransitionEventHandler = EventHandler; 167 | type GenericEventHandler = EventHandler; 168 | 169 | interface DOMAttributes { 170 | // Clipboard Events 171 | onCopy?:ClipboardEventHandler; 172 | onCut?:ClipboardEventHandler; 173 | onPaste?:ClipboardEventHandler; 174 | 175 | // Composition Events 176 | onCompositionEnd?:CompositionEventHandler; 177 | onCompositionStart?:CompositionEventHandler; 178 | onCompositionUpdate?:CompositionEventHandler; 179 | 180 | // Focus Events 181 | onFocus?:FocusEventHandler; 182 | onBlur?:FocusEventHandler; 183 | 184 | // Form Events 185 | onChange?:GenericEventHandler; 186 | onInput?:GenericEventHandler; 187 | onSubmit?:GenericEventHandler; 188 | 189 | // Keyboard Events 190 | onKeyDown?:KeyboardEventHandler; 191 | onKeyPress?:KeyboardEventHandler; 192 | onKeyUp?:KeyboardEventHandler; 193 | 194 | // Media Events 195 | onAbort?:GenericEventHandler; 196 | onCanPlay?:GenericEventHandler; 197 | onCanPlayThrough?:GenericEventHandler; 198 | onDurationChange?:GenericEventHandler; 199 | onEmptied?:GenericEventHandler; 200 | onEncrypted?:GenericEventHandler; 201 | onEnded?:GenericEventHandler; 202 | onLoadedData?:GenericEventHandler; 203 | onLoadedMetadata?:GenericEventHandler; 204 | onLoadStart?:GenericEventHandler; 205 | onPause?:GenericEventHandler; 206 | onPlay?:GenericEventHandler; 207 | onPlaying?:GenericEventHandler; 208 | onProgress?:GenericEventHandler; 209 | onRateChange?:GenericEventHandler; 210 | onSeeked?:GenericEventHandler; 211 | onSeeking?:GenericEventHandler; 212 | onStalled?:GenericEventHandler; 213 | onSuspend?:GenericEventHandler; 214 | onTimeUpdate?:GenericEventHandler; 215 | onVolumeChange?:GenericEventHandler; 216 | onWaiting?:GenericEventHandler; 217 | 218 | // MouseEvents 219 | onClick?:MouseEventHandler; 220 | onContextMenu?:MouseEventHandler; 221 | onDoubleClick?:MouseEventHandler; 222 | onDrag?:DragEventHandler; 223 | onDragEnd?:DragEventHandler; 224 | onDragEnter?:DragEventHandler; 225 | onDragExit?:DragEventHandler; 226 | onDragLeave?:DragEventHandler; 227 | onDragOver?:DragEventHandler; 228 | onDragStart?:DragEventHandler; 229 | onDrop?:DragEventHandler; 230 | onMouseDown?:MouseEventHandler; 231 | onMouseEnter?:MouseEventHandler; 232 | onMouseLeave?:MouseEventHandler; 233 | onMouseMove?:MouseEventHandler; 234 | onMouseOut?:MouseEventHandler; 235 | onMouseOver?:MouseEventHandler; 236 | onMouseUp?:MouseEventHandler; 237 | 238 | // Selection Events 239 | onSelect?:GenericEventHandler; 240 | 241 | // Touch Events 242 | onTouchCancel?:TouchEventHandler; 243 | onTouchEnd?:TouchEventHandler; 244 | onTouchMove?:TouchEventHandler; 245 | onTouchStart?:TouchEventHandler; 246 | 247 | // UI Events 248 | onScroll?:UIEventHandler; 249 | 250 | // Wheel Events 251 | onWheel?:WheelEventHandler; 252 | 253 | // Animation Events 254 | onAnimationStart?:AnimationEventHandler; 255 | onAnimationEnd?:AnimationEventHandler; 256 | onAnimationIteration?:AnimationEventHandler; 257 | 258 | // Transition Events 259 | onTransitionEnd?:TransitionEventHandler; 260 | } 261 | 262 | interface HTMLAttributes extends preact.PreactHTMLAttributes, DOMAttributes { 263 | // Standard HTML Attributes 264 | accept?:string; 265 | acceptCharset?:string; 266 | accessKey?:string; 267 | action?:string; 268 | allowFullScreen?:boolean; 269 | allowTransparency?:boolean; 270 | alt?:string; 271 | async?:boolean; 272 | autocomplete?:string; 273 | autofocus?:boolean; 274 | autoPlay?:boolean; 275 | capture?:boolean; 276 | cellPadding?:number | string; 277 | cellSpacing?:number | string; 278 | charSet?:string; 279 | challenge?:string; 280 | checked?:boolean; 281 | class?:string | { [key:string]: boolean }; 282 | className?:string | { [key:string]: boolean }; 283 | cols?:number; 284 | colSpan?:number; 285 | content?:string; 286 | contentEditable?:boolean; 287 | contextMenu?:string; 288 | controls?:boolean; 289 | coords?:string; 290 | crossOrigin?:string; 291 | data?:string; 292 | dateTime?:string; 293 | default?:boolean; 294 | defer?:boolean; 295 | dir?:string; 296 | disabled?:boolean; 297 | download?:any; 298 | draggable?:boolean; 299 | encType?:string; 300 | form?:string; 301 | formAction?:string; 302 | formEncType?:string; 303 | formMethod?:string; 304 | formNoValidate?:boolean; 305 | formTarget?:string; 306 | frameBorder?:number | string; 307 | headers?:string; 308 | height?:number | string; 309 | hidden?:boolean; 310 | high?:number; 311 | href?:string; 312 | hrefLang?:string; 313 | for?:string; 314 | httpEquiv?:string; 315 | icon?:string; 316 | id?:string; 317 | inputMode?:string; 318 | integrity?:string; 319 | is?:string; 320 | keyParams?:string; 321 | keyType?:string; 322 | kind?:string; 323 | label?:string; 324 | lang?:string; 325 | list?:string; 326 | loop?:boolean; 327 | low?:number; 328 | manifest?:string; 329 | marginHeight?:number; 330 | marginWidth?:number; 331 | max?:number | string; 332 | maxLength?:number; 333 | media?:string; 334 | mediaGroup?:string; 335 | method?:string; 336 | min?:number | string; 337 | minLength?:number; 338 | multiple?:boolean; 339 | muted?:boolean; 340 | name?:string; 341 | noValidate?:boolean; 342 | open?:boolean; 343 | optimum?:number; 344 | pattern?:string; 345 | placeholder?:string; 346 | poster?:string; 347 | preload?:string; 348 | radioGroup?:string; 349 | readOnly?:boolean; 350 | rel?:string; 351 | required?:boolean; 352 | role?:string; 353 | rows?:number; 354 | rowSpan?:number; 355 | sandbox?:string; 356 | scope?:string; 357 | scoped?:boolean; 358 | scrolling?:string; 359 | seamless?:boolean; 360 | selected?:boolean; 361 | shape?:string; 362 | size?:number; 363 | sizes?:string; 364 | span?:number; 365 | spellCheck?:boolean; 366 | src?:string; 367 | srcset?:string; 368 | srcDoc?:string; 369 | srcLang?:string; 370 | srcSet?:string; 371 | start?:number; 372 | step?:number | string; 373 | style?:any; 374 | summary?:string; 375 | tabIndex?:number; 376 | target?:string; 377 | title?:string; 378 | type?:string; 379 | useMap?:string; 380 | value?:string | string[]; 381 | width?:number | string; 382 | wmode?:string; 383 | wrap?:string; 384 | 385 | // RDFa Attributes 386 | about?:string; 387 | datatype?:string; 388 | inlist?:any; 389 | prefix?:string; 390 | property?:string; 391 | resource?:string; 392 | typeof?:string; 393 | vocab?:string; 394 | } 395 | 396 | interface IntrinsicElements { 397 | // HTML 398 | a:HTMLAttributes; 399 | abbr:HTMLAttributes; 400 | address:HTMLAttributes; 401 | area:HTMLAttributes; 402 | article:HTMLAttributes; 403 | aside:HTMLAttributes; 404 | audio:HTMLAttributes; 405 | b:HTMLAttributes; 406 | base:HTMLAttributes; 407 | bdi:HTMLAttributes; 408 | bdo:HTMLAttributes; 409 | big:HTMLAttributes; 410 | blockquote:HTMLAttributes; 411 | body:HTMLAttributes; 412 | br:HTMLAttributes; 413 | button:HTMLAttributes; 414 | canvas:HTMLAttributes; 415 | caption:HTMLAttributes; 416 | cite:HTMLAttributes; 417 | code:HTMLAttributes; 418 | col:HTMLAttributes; 419 | colgroup:HTMLAttributes; 420 | data:HTMLAttributes; 421 | datalist:HTMLAttributes; 422 | dd:HTMLAttributes; 423 | del:HTMLAttributes; 424 | details:HTMLAttributes; 425 | dfn:HTMLAttributes; 426 | dialog:HTMLAttributes; 427 | div:HTMLAttributes; 428 | dl:HTMLAttributes; 429 | dt:HTMLAttributes; 430 | em:HTMLAttributes; 431 | embed:HTMLAttributes; 432 | fieldset:HTMLAttributes; 433 | figcaption:HTMLAttributes; 434 | figure:HTMLAttributes; 435 | footer:HTMLAttributes; 436 | form:HTMLAttributes; 437 | h1:HTMLAttributes; 438 | h2:HTMLAttributes; 439 | h3:HTMLAttributes; 440 | h4:HTMLAttributes; 441 | h5:HTMLAttributes; 442 | h6:HTMLAttributes; 443 | head:HTMLAttributes; 444 | header:HTMLAttributes; 445 | hr:HTMLAttributes; 446 | html:HTMLAttributes; 447 | i:HTMLAttributes; 448 | iframe:HTMLAttributes; 449 | img:HTMLAttributes; 450 | input:HTMLAttributes; 451 | ins:HTMLAttributes; 452 | kbd:HTMLAttributes; 453 | keygen:HTMLAttributes; 454 | label:HTMLAttributes; 455 | legend:HTMLAttributes; 456 | li:HTMLAttributes; 457 | link:HTMLAttributes; 458 | main:HTMLAttributes; 459 | map:HTMLAttributes; 460 | mark:HTMLAttributes; 461 | menu:HTMLAttributes; 462 | menuitem:HTMLAttributes; 463 | meta:HTMLAttributes; 464 | meter:HTMLAttributes; 465 | nav:HTMLAttributes; 466 | noscript:HTMLAttributes; 467 | object:HTMLAttributes; 468 | ol:HTMLAttributes; 469 | optgroup:HTMLAttributes; 470 | option:HTMLAttributes; 471 | output:HTMLAttributes; 472 | p:HTMLAttributes; 473 | param:HTMLAttributes; 474 | picture:HTMLAttributes; 475 | pre:HTMLAttributes; 476 | progress:HTMLAttributes; 477 | q:HTMLAttributes; 478 | rp:HTMLAttributes; 479 | rt:HTMLAttributes; 480 | ruby:HTMLAttributes; 481 | s:HTMLAttributes; 482 | samp:HTMLAttributes; 483 | script:HTMLAttributes; 484 | section:HTMLAttributes; 485 | select:HTMLAttributes; 486 | small:HTMLAttributes; 487 | source:HTMLAttributes; 488 | span:HTMLAttributes; 489 | strong:HTMLAttributes; 490 | style:HTMLAttributes; 491 | sub:HTMLAttributes; 492 | summary:HTMLAttributes; 493 | sup:HTMLAttributes; 494 | table:HTMLAttributes; 495 | tbody:HTMLAttributes; 496 | td:HTMLAttributes; 497 | textarea:HTMLAttributes; 498 | tfoot:HTMLAttributes; 499 | th:HTMLAttributes; 500 | thead:HTMLAttributes; 501 | time:HTMLAttributes; 502 | title:HTMLAttributes; 503 | tr:HTMLAttributes; 504 | track:HTMLAttributes; 505 | u:HTMLAttributes; 506 | ul:HTMLAttributes; 507 | "var":HTMLAttributes; 508 | video:HTMLAttributes; 509 | wbr:HTMLAttributes; 510 | 511 | //SVG 512 | svg:SVGAttributes; 513 | 514 | circle:SVGAttributes; 515 | clipPath:SVGAttributes; 516 | defs:SVGAttributes; 517 | ellipse:SVGAttributes; 518 | feBlend:SVGAttributes; 519 | feColorMatrix:SVGAttributes; 520 | feComponentTransfer:SVGAttributes; 521 | feComposite:SVGAttributes; 522 | feConvolveMatrix:SVGAttributes; 523 | feDiffuseLighting:SVGAttributes; 524 | feDisplacementMap:SVGAttributes; 525 | feFlood:SVGAttributes; 526 | feGaussianBlur:SVGAttributes; 527 | feImage:SVGAttributes; 528 | feMerge:SVGAttributes; 529 | feMergeNode:SVGAttributes; 530 | feMorphology:SVGAttributes; 531 | feOffset:SVGAttributes; 532 | feSpecularLighting:SVGAttributes; 533 | feTile:SVGAttributes; 534 | feTurbulence:SVGAttributes; 535 | filter:SVGAttributes; 536 | foreignObject:SVGAttributes; 537 | g:SVGAttributes; 538 | image:SVGAttributes; 539 | line:SVGAttributes; 540 | linearGradient:SVGAttributes; 541 | marker:SVGAttributes; 542 | mask:SVGAttributes; 543 | path:SVGAttributes; 544 | pattern:SVGAttributes; 545 | polygon:SVGAttributes; 546 | polyline:SVGAttributes; 547 | radialGradient:SVGAttributes; 548 | rect:SVGAttributes; 549 | stop:SVGAttributes; 550 | symbol:SVGAttributes; 551 | text:SVGAttributes; 552 | tspan:SVGAttributes; 553 | use:SVGAttributes; 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /test/browser/lifecycle.js: -------------------------------------------------------------------------------- 1 | import { h, render, rerender, Component } from '../../src/preact'; 2 | /** @jsx h */ 3 | 4 | let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) ); 5 | 6 | const EMPTY_CHILDREN = []; 7 | 8 | describe('Lifecycle methods', () => { 9 | let scratch; 10 | 11 | before( () => { 12 | scratch = document.createElement('div'); 13 | (document.body || document.documentElement).appendChild(scratch); 14 | }); 15 | 16 | beforeEach( () => { 17 | scratch.innerHTML = ''; 18 | }); 19 | 20 | after( () => { 21 | scratch.parentNode.removeChild(scratch); 22 | scratch = null; 23 | }); 24 | 25 | 26 | describe('#componentWillUpdate', () => { 27 | it('should NOT be called on initial render', () => { 28 | class ReceivePropsComponent extends Component { 29 | componentWillUpdate() {} 30 | render() { 31 | return
    ; 32 | } 33 | } 34 | sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate'); 35 | render(, scratch); 36 | expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have.been.called; 37 | }); 38 | 39 | it('should be called when rerender with new props from parent', () => { 40 | let doRender; 41 | class Outer extends Component { 42 | constructor(p, c) { 43 | super(p, c); 44 | this.state = { i: 0 }; 45 | } 46 | componentDidMount() { 47 | doRender = () => this.setState({ i: this.state.i + 1 }); 48 | } 49 | render(props, { i }) { 50 | return ; 51 | } 52 | } 53 | class Inner extends Component { 54 | componentWillUpdate(nextProps, nextState) { 55 | expect(nextProps).to.be.deep.equal({ children:EMPTY_CHILDREN, i: 1 }); 56 | expect(nextState).to.be.deep.equal({}); 57 | } 58 | render() { 59 | return
    ; 60 | } 61 | } 62 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 63 | sinon.spy(Outer.prototype, 'componentDidMount'); 64 | 65 | // Initial render 66 | render(, scratch); 67 | expect(Inner.prototype.componentWillUpdate).not.to.have.been.called; 68 | 69 | // Rerender inner with new props 70 | doRender(); 71 | rerender(); 72 | expect(Inner.prototype.componentWillUpdate).to.have.been.called; 73 | }); 74 | 75 | it('should be called on new state', () => { 76 | let doRender; 77 | class ReceivePropsComponent extends Component { 78 | componentWillUpdate() {} 79 | componentDidMount() { 80 | doRender = () => this.setState({ i: this.state.i + 1 }); 81 | } 82 | render() { 83 | return
    ; 84 | } 85 | } 86 | sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate'); 87 | render(, scratch); 88 | expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have.been.called; 89 | 90 | doRender(); 91 | rerender(); 92 | expect(ReceivePropsComponent.prototype.componentWillUpdate).to.have.been.called; 93 | }); 94 | }); 95 | 96 | describe('#componentWillReceiveProps', () => { 97 | it('should NOT be called on initial render', () => { 98 | class ReceivePropsComponent extends Component { 99 | componentWillReceiveProps() {} 100 | render() { 101 | return
    ; 102 | } 103 | } 104 | sinon.spy(ReceivePropsComponent.prototype, 'componentWillReceiveProps'); 105 | render(, scratch); 106 | expect(ReceivePropsComponent.prototype.componentWillReceiveProps).not.to.have.been.called; 107 | }); 108 | 109 | it('should be called when rerender with new props from parent', () => { 110 | let doRender; 111 | class Outer extends Component { 112 | constructor(p, c) { 113 | super(p, c); 114 | this.state = { i: 0 }; 115 | } 116 | componentDidMount() { 117 | doRender = () => this.setState({ i: this.state.i + 1 }); 118 | } 119 | render(props, { i }) { 120 | return ; 121 | } 122 | } 123 | class Inner extends Component { 124 | componentWillMount() { 125 | expect(this.props.i).to.be.equal(0); 126 | } 127 | componentWillReceiveProps(nextProps) { 128 | expect(nextProps.i).to.be.equal(1); 129 | } 130 | render() { 131 | return
    ; 132 | } 133 | } 134 | sinon.spy(Inner.prototype, 'componentWillReceiveProps'); 135 | sinon.spy(Outer.prototype, 'componentDidMount'); 136 | 137 | // Initial render 138 | render(, scratch); 139 | expect(Inner.prototype.componentWillReceiveProps).not.to.have.been.called; 140 | 141 | // Rerender inner with new props 142 | doRender(); 143 | rerender(); 144 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.called; 145 | }); 146 | 147 | it('should be called in right execution order', () => { 148 | let doRender; 149 | class Outer extends Component { 150 | constructor(p, c) { 151 | super(p, c); 152 | this.state = { i: 0 }; 153 | } 154 | componentDidMount() { 155 | doRender = () => this.setState({ i: this.state.i + 1 }); 156 | } 157 | render(props, { i }) { 158 | return ; 159 | } 160 | } 161 | class Inner extends Component { 162 | componentDidUpdate() { 163 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.called; 164 | expect(Inner.prototype.componentWillUpdate).to.have.been.called; 165 | } 166 | componentWillReceiveProps() { 167 | expect(Inner.prototype.componentWillUpdate).not.to.have.been.called; 168 | expect(Inner.prototype.componentDidUpdate).not.to.have.been.called; 169 | } 170 | componentWillUpdate() { 171 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.called; 172 | expect(Inner.prototype.componentDidUpdate).not.to.have.been.called; 173 | } 174 | render() { 175 | return
    ; 176 | } 177 | } 178 | sinon.spy(Inner.prototype, 'componentWillReceiveProps'); 179 | sinon.spy(Inner.prototype, 'componentDidUpdate'); 180 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 181 | sinon.spy(Outer.prototype, 'componentDidMount'); 182 | 183 | render(, scratch); 184 | doRender(); 185 | rerender(); 186 | 187 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledBefore(Inner.prototype.componentWillUpdate); 188 | expect(Inner.prototype.componentWillUpdate).to.have.been.calledBefore(Inner.prototype.componentDidUpdate); 189 | }); 190 | }); 191 | 192 | 193 | describe('top-level componentWillUnmount', () => { 194 | it('should invoke componentWillUnmount for top-level components', () => { 195 | class Foo extends Component { 196 | componentDidMount() {} 197 | componentWillUnmount() {} 198 | } 199 | class Bar extends Component { 200 | componentDidMount() {} 201 | componentWillUnmount() {} 202 | } 203 | spyAll(Foo.prototype); 204 | spyAll(Bar.prototype); 205 | 206 | render(, scratch, scratch.lastChild); 207 | expect(Foo.prototype.componentDidMount, 'initial render').to.have.been.calledOnce; 208 | 209 | render(, scratch, scratch.lastChild); 210 | expect(Foo.prototype.componentWillUnmount, 'when replaced').to.have.been.calledOnce; 211 | expect(Bar.prototype.componentDidMount, 'when replaced').to.have.been.calledOnce; 212 | 213 | render(
    , scratch, scratch.lastChild); 214 | expect(Bar.prototype.componentWillUnmount, 'when removed').to.have.been.calledOnce; 215 | }); 216 | }); 217 | 218 | 219 | let _it = it; 220 | describe('#constructor and component(Did|Will)(Mount|Unmount)', () => { 221 | /* global DISABLE_FLAKEY */ 222 | let it = DISABLE_FLAKEY ? xit : _it; 223 | 224 | let setState; 225 | class Outer extends Component { 226 | constructor(p, c) { 227 | super(p, c); 228 | this.state = { show:true }; 229 | setState = s => this.setState(s); 230 | } 231 | render(props, { show }) { 232 | return ( 233 |
    234 | { show && ( 235 | 236 | ) } 237 |
    238 | ); 239 | } 240 | } 241 | 242 | class LifecycleTestComponent extends Component { 243 | constructor(p, c) { super(p, c); this._constructor(); } 244 | _constructor() {} 245 | componentWillMount() {} 246 | componentDidMount() {} 247 | componentWillUnmount() {} 248 | componentDidUnmount() {} 249 | render() { return
    ; } 250 | } 251 | 252 | class Inner extends LifecycleTestComponent { 253 | render() { 254 | return ( 255 |
    256 | 257 |
    258 | ); 259 | } 260 | } 261 | 262 | class InnerMost extends LifecycleTestComponent { 263 | render() { return
    ; } 264 | } 265 | 266 | let spies = ['_constructor', 'componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount']; 267 | 268 | let verifyLifycycleMethods = (TestComponent) => { 269 | let proto = TestComponent.prototype; 270 | spies.forEach( s => sinon.spy(proto, s) ); 271 | let reset = () => spies.forEach( s => proto[s].reset() ); 272 | 273 | it('should be invoked for components on initial render', () => { 274 | reset(); 275 | render(, scratch); 276 | expect(proto._constructor).to.have.been.called; 277 | expect(proto.componentDidMount).to.have.been.called; 278 | expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); 279 | expect(proto.componentDidMount).to.have.been.called; 280 | }); 281 | 282 | it('should be invoked for components on unmount', () => { 283 | reset(); 284 | setState({ show:false }); 285 | rerender(); 286 | 287 | expect(proto.componentDidUnmount).to.have.been.called; 288 | expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); 289 | expect(proto.componentDidUnmount).to.have.been.called; 290 | }); 291 | 292 | it('should be invoked for components on re-render', () => { 293 | reset(); 294 | setState({ show:true }); 295 | rerender(); 296 | 297 | expect(proto._constructor).to.have.been.called; 298 | expect(proto.componentDidMount).to.have.been.called; 299 | expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); 300 | expect(proto.componentDidMount).to.have.been.called; 301 | }); 302 | }; 303 | 304 | describe('inner components', () => { 305 | verifyLifycycleMethods(Inner); 306 | }); 307 | 308 | describe('innermost components', () => { 309 | verifyLifycycleMethods(InnerMost); 310 | }); 311 | 312 | describe('when shouldComponentUpdate() returns false', () => { 313 | let setState; 314 | 315 | class Outer extends Component { 316 | constructor() { 317 | super(); 318 | this.state = { show:true }; 319 | setState = s => this.setState(s); 320 | } 321 | render(props, { show }) { 322 | return ( 323 |
    324 | { show && ( 325 |
    326 | 327 |
    328 | ) } 329 |
    330 | ); 331 | } 332 | } 333 | 334 | class Inner extends Component { 335 | shouldComponentUpdate(){ return false; } 336 | componentWillMount() {} 337 | componentDidMount() {} 338 | componentWillUnmount() {} 339 | componentDidUnmount() {} 340 | render() { 341 | return
    ; 342 | } 343 | } 344 | 345 | let proto = Inner.prototype; 346 | let spies = ['componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount']; 347 | spies.forEach( s => sinon.spy(proto, s) ); 348 | 349 | let reset = () => spies.forEach( s => proto[s].reset() ); 350 | 351 | beforeEach( () => reset() ); 352 | 353 | it('should be invoke normally on initial mount', () => { 354 | render(, scratch); 355 | expect(proto.componentWillMount).to.have.been.called; 356 | expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); 357 | expect(proto.componentDidMount).to.have.been.called; 358 | }); 359 | 360 | it('should be invoked normally on unmount', () => { 361 | setState({ show:false }); 362 | rerender(); 363 | 364 | expect(proto.componentWillUnmount).to.have.been.called; 365 | expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); 366 | expect(proto.componentDidUnmount).to.have.been.called; 367 | }); 368 | 369 | it('should still invoke mount for shouldComponentUpdate():false', () => { 370 | setState({ show:true }); 371 | rerender(); 372 | 373 | expect(proto.componentWillMount).to.have.been.called; 374 | expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); 375 | expect(proto.componentDidMount).to.have.been.called; 376 | }); 377 | 378 | it('should still invoke unmount for shouldComponentUpdate():false', () => { 379 | setState({ show:false }); 380 | rerender(); 381 | 382 | expect(proto.componentWillUnmount).to.have.been.called; 383 | expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); 384 | expect(proto.componentDidUnmount).to.have.been.called; 385 | }); 386 | }); 387 | }); 388 | 389 | describe('Lifecycle DOM Timing', () => { 390 | it('should be invoked when dom does (DidMount, WillUnmount) or does not (WillMount, DidUnmount) exist', () => { 391 | let setState; 392 | class Outer extends Component { 393 | constructor() { 394 | super(); 395 | this.state = { show:true }; 396 | setState = s => { 397 | this.setState(s); 398 | this.forceUpdate(); 399 | }; 400 | } 401 | componentWillMount() { 402 | expect(document.getElementById('OuterDiv'), 'Outer componentWillMount').to.not.exist; 403 | } 404 | componentDidMount() { 405 | expect(document.getElementById('OuterDiv'), 'Outer componentDidMount').to.exist; 406 | } 407 | componentWillUnmount() { 408 | expect(document.getElementById('OuterDiv'), 'Outer componentWillUnmount').to.exist; 409 | } 410 | componentDidUnmount() { 411 | expect(document.getElementById('OuterDiv'), 'Outer componentDidUnmount').to.not.exist; 412 | } 413 | render(props, { show }) { 414 | return ( 415 |
    416 | { show && ( 417 |
    418 | 419 |
    420 | ) } 421 |
    422 | ); 423 | } 424 | } 425 | 426 | class Inner extends Component { 427 | componentWillMount() { 428 | expect(document.getElementById('InnerDiv'), 'Inner componentWillMount').to.not.exist; 429 | } 430 | componentDidMount() { 431 | expect(document.getElementById('InnerDiv'), 'Inner componentDidMount').to.exist; 432 | } 433 | componentWillUnmount() { 434 | // @TODO Component mounted into elements (non-components) 435 | // are currently unmounted after those elements, so their 436 | // DOM is unmounted prior to the method being called. 437 | //expect(document.getElementById('InnerDiv'), 'Inner componentWillUnmount').to.exist; 438 | } 439 | componentDidUnmount() { 440 | expect(document.getElementById('InnerDiv'), 'Inner componentDidUnmount').to.not.exist; 441 | } 442 | 443 | render() { 444 | return
    ; 445 | } 446 | } 447 | 448 | let proto = Inner.prototype; 449 | let spies = ['componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount']; 450 | spies.forEach( s => sinon.spy(proto, s) ); 451 | 452 | let reset = () => spies.forEach( s => proto[s].reset() ); 453 | 454 | render(, scratch); 455 | expect(proto.componentWillMount).to.have.been.called; 456 | expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); 457 | expect(proto.componentDidMount).to.have.been.called; 458 | 459 | reset(); 460 | setState({ show:false }); 461 | 462 | expect(proto.componentWillUnmount).to.have.been.called; 463 | expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); 464 | expect(proto.componentDidUnmount).to.have.been.called; 465 | 466 | reset(); 467 | setState({ show:true }); 468 | 469 | expect(proto.componentWillMount).to.have.been.called; 470 | expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); 471 | expect(proto.componentDidMount).to.have.been.called; 472 | }); 473 | 474 | it('should remove this.base for HOC', () => { 475 | let createComponent = (name, fn) => { 476 | class C extends Component { 477 | componentWillUnmount() { 478 | expect(this.base, `${name}.componentWillUnmount`).to.exist; 479 | } 480 | componentDidUnmount() { 481 | expect(this.base, `${name}.componentDidUnmount`).not.to.exist; 482 | } 483 | render(props) { return fn(props); } 484 | } 485 | spyAll(C.prototype); 486 | return C; 487 | }; 488 | 489 | class Wrapper extends Component { 490 | render({ children }) { 491 | return
    {children}
    ; 492 | } 493 | } 494 | 495 | let One = createComponent('One', () => one ); 496 | let Two = createComponent('Two', () => two ); 497 | let Three = createComponent('Three', () => three ); 498 | 499 | let components = [One, Two, Three]; 500 | 501 | let Selector = createComponent('Selector', ({ page }) => { 502 | let Child = components[page]; 503 | return ; 504 | }); 505 | 506 | class App extends Component { 507 | render(_, { page }) { 508 | return ; 509 | } 510 | } 511 | 512 | let app; 513 | render( app=c } />, scratch); 514 | 515 | for (let i=0; i<20; i++) { 516 | app.setState({ page: i%components.length }); 517 | app.forceUpdate(); 518 | } 519 | }); 520 | }); 521 | }); 522 | -------------------------------------------------------------------------------- /test/browser/render.js: -------------------------------------------------------------------------------- 1 | /* global DISABLE_FLAKEY */ 2 | 3 | import { h, render, Component } from '../../src/preact'; 4 | /** @jsx h */ 5 | 6 | function getAttributes(node) { 7 | let attrs = {}; 8 | for (let i=node.attributes.length; i--; ) { 9 | attrs[node.attributes[i].name] = node.attributes[i].value; 10 | } 11 | return attrs; 12 | } 13 | 14 | // hacky normalization of attribute order across browsers. 15 | function sortAttributes(html) { 16 | return html.replace(/<([a-z0-9-]+)((?:\s[a-z0-9:_.-]+=".*?")+)((?:\s*\/)?>)/gi, (s, pre, attrs, after) => { 17 | let list = attrs.match(/\s[a-z0-9:_.-]+=".*?"/gi).sort( (a, b) => a>b ? 1 : -1 ); 18 | if (~after.indexOf('/')) after = '>'; 19 | return '<' + pre + list.join('') + after; 20 | }); 21 | } 22 | 23 | describe('render()', () => { 24 | let scratch; 25 | 26 | before( () => { 27 | scratch = document.createElement('div'); 28 | (document.body || document.documentElement).appendChild(scratch); 29 | }); 30 | 31 | beforeEach( () => { 32 | scratch.innerHTML = ''; 33 | }); 34 | 35 | after( () => { 36 | scratch.parentNode.removeChild(scratch); 37 | scratch = null; 38 | }); 39 | 40 | it('should create empty nodes (<* />)', () => { 41 | render(
    , scratch); 42 | expect(scratch.childNodes) 43 | .to.have.length(1) 44 | .and.to.have.deep.property('0.nodeName', 'DIV'); 45 | 46 | scratch.innerHTML = ''; 47 | 48 | render(, scratch); 49 | expect(scratch.childNodes) 50 | .to.have.length(1) 51 | .and.to.have.deep.property('0.nodeName', 'SPAN'); 52 | 53 | scratch.innerHTML = ''; 54 | 55 | render(, scratch); 56 | render(, scratch); 57 | expect(scratch.childNodes).to.have.length(2); 58 | expect(scratch.childNodes[0]).to.have.property('nodeName', 'FOO'); 59 | expect(scratch.childNodes[1]).to.have.property('nodeName', 'X-BAR'); 60 | }); 61 | 62 | it('should nest empty nodes', () => { 63 | render(( 64 |
    65 | 66 | 67 | 68 |
    69 | ), scratch); 70 | 71 | expect(scratch.childNodes) 72 | .to.have.length(1) 73 | .and.to.have.deep.property('0.nodeName', 'DIV'); 74 | 75 | let c = scratch.childNodes[0].childNodes; 76 | expect(c).to.have.length(3); 77 | expect(c).to.have.deep.property('0.nodeName', 'SPAN'); 78 | expect(c).to.have.deep.property('1.nodeName', 'FOO'); 79 | expect(c).to.have.deep.property('2.nodeName', 'X-BAR'); 80 | }); 81 | 82 | it('should not render falsey values', () => { 83 | render(( 84 |
    85 | {null},{undefined},{false},{0},{NaN} 86 |
    87 | ), scratch); 88 | 89 | expect(scratch.firstChild).to.have.property('innerHTML', ',,,0,NaN'); 90 | }); 91 | 92 | it('should clear falsey attributes', () => { 93 | let root = render(( 94 |
    95 | ), scratch); 96 | 97 | render(( 98 |
    99 | ), scratch, root); 100 | 101 | expect(getAttributes(scratch.firstChild), 'from previous truthy values').to.eql({ 102 | a0: '0', 103 | anan: 'NaN' 104 | }); 105 | 106 | scratch.innerHTML = ''; 107 | 108 | render(( 109 |
    110 | ), scratch); 111 | 112 | expect(getAttributes(scratch.firstChild), 'initial render').to.eql({ 113 | a0: '0', 114 | anan: 'NaN' 115 | }); 116 | }); 117 | 118 | it('should clear falsey input values', () => { 119 | let root = render(( 120 |
    121 | 122 | 123 | 124 | 125 |
    126 | ), scratch); 127 | 128 | expect(root.children[0]).to.have.property('value', '0'); 129 | expect(root.children[1]).to.have.property('value', 'false'); 130 | expect(root.children[2]).to.have.property('value', ''); 131 | expect(root.children[3]).to.have.property('value', ''); 132 | }); 133 | 134 | it('should clear falsey DOM properties', () => { 135 | let root; 136 | function test(val) { 137 | root = render(( 138 |
    139 | 140 | 141 | 142 | ), scratch, root); 143 | } 144 | 145 | test('2'); 146 | test(false); 147 | expect(scratch).to.have.property('innerHTML', '
    ', 'for false'); 148 | 149 | test('3'); 150 | test(null); 151 | expect(scratch).to.have.property('innerHTML', '
    ', 'for null'); 152 | 153 | test('4'); 154 | test(undefined); 155 | expect(scratch).to.have.property('innerHTML', '
    ', 'for undefined'); 156 | }); 157 | 158 | it('should apply string attributes', () => { 159 | render(
    , scratch); 160 | 161 | let div = scratch.childNodes[0]; 162 | expect(div).to.have.deep.property('attributes.length', 2); 163 | 164 | expect(div).to.have.deep.property('attributes[0].name', 'foo'); 165 | expect(div).to.have.deep.property('attributes[0].value', 'bar'); 166 | 167 | expect(div).to.have.deep.property('attributes[1].name', 'data-foo'); 168 | expect(div).to.have.deep.property('attributes[1].value', 'databar'); 169 | }); 170 | 171 | it('should apply class as String', () => { 172 | render(
    , scratch); 173 | expect(scratch.childNodes[0]).to.have.property('className', 'foo'); 174 | }); 175 | 176 | it('should alias className to class', () => { 177 | render(
    , scratch); 178 | expect(scratch.childNodes[0]).to.have.property('className', 'bar'); 179 | }); 180 | 181 | it('should apply style as String', () => { 182 | render(
    , scratch); 183 | expect(scratch.childNodes[0]).to.have.deep.property('style.cssText') 184 | .that.matches(/top\s*:\s*5px\s*/) 185 | .and.matches(/position\s*:\s*relative\s*/); 186 | }); 187 | 188 | it('should only register on* functions as handlers', () => { 189 | let click = () => {}, 190 | onclick = () => {}; 191 | 192 | let proto = document.createElement('div').constructor.prototype; 193 | 194 | sinon.spy(proto, 'addEventListener'); 195 | 196 | render(
    , scratch); 197 | 198 | expect(scratch.childNodes[0]).to.have.deep.property('attributes.length', 0); 199 | 200 | expect(proto.addEventListener).to.have.been.calledOnce 201 | .and.to.have.been.calledWithExactly('click', sinon.match.func, false); 202 | 203 | proto.addEventListener.restore(); 204 | }); 205 | 206 | it('should add and remove event handlers', () => { 207 | let click = sinon.spy(), 208 | mousedown = sinon.spy(); 209 | 210 | let proto = document.createElement('div').constructor.prototype; 211 | sinon.spy(proto, 'addEventListener'); 212 | sinon.spy(proto, 'removeEventListener'); 213 | 214 | function fireEvent(on, type) { 215 | let e = document.createEvent('Event'); 216 | e.initEvent(type, true, true); 217 | on.dispatchEvent(e); 218 | } 219 | 220 | render(
    click(1) } onMouseDown={ mousedown } />, scratch); 221 | 222 | expect(proto.addEventListener).to.have.been.calledTwice 223 | .and.to.have.been.calledWith('click') 224 | .and.calledWith('mousedown'); 225 | 226 | fireEvent(scratch.childNodes[0], 'click'); 227 | expect(click).to.have.been.calledOnce 228 | .and.calledWith(1); 229 | 230 | proto.addEventListener.reset(); 231 | click.reset(); 232 | 233 | render(
    click(2) } />, scratch, scratch.firstChild); 234 | 235 | expect(proto.addEventListener).not.to.have.been.called; 236 | 237 | expect(proto.removeEventListener) 238 | .to.have.been.calledOnce 239 | .and.calledWith('mousedown'); 240 | 241 | fireEvent(scratch.childNodes[0], 'click'); 242 | expect(click).to.have.been.calledOnce 243 | .and.to.have.been.calledWith(2); 244 | 245 | fireEvent(scratch.childNodes[0], 'mousedown'); 246 | expect(mousedown).not.to.have.been.called; 247 | 248 | proto.removeEventListener.reset(); 249 | click.reset(); 250 | mousedown.reset(); 251 | 252 | render(
    , scratch, scratch.firstChild); 253 | 254 | expect(proto.removeEventListener) 255 | .to.have.been.calledOnce 256 | .and.calledWith('click'); 257 | 258 | fireEvent(scratch.childNodes[0], 'click'); 259 | expect(click).not.to.have.been.called; 260 | 261 | proto.addEventListener.restore(); 262 | proto.removeEventListener.restore(); 263 | }); 264 | 265 | it('should use capturing for events that do not bubble', () => { 266 | let click = sinon.spy(), 267 | focus = sinon.spy(); 268 | 269 | let root = render(( 270 |
    271 |
    273 | ), scratch); 274 | 275 | root.firstElementChild.click(); 276 | root.firstElementChild.focus(); 277 | 278 | expect(click, 'click').to.have.been.calledOnce; 279 | 280 | if (DISABLE_FLAKEY!==true) { 281 | // Focus delegation requires a 50b hack I'm not sure we want to incur 282 | expect(focus, 'focus').to.have.been.calledOnce; 283 | 284 | // IE doesn't set it 285 | expect(click).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing 286 | expect(focus).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing 287 | } 288 | }); 289 | 290 | it('should serialize style objects', () => { 291 | let root = render(( 292 |
    301 | test 302 |
    303 | ), scratch); 304 | 305 | let { style } = scratch.childNodes[0]; 306 | expect(style).to.have.property('color').that.equals('rgb(255, 255, 255)'); 307 | expect(style).to.have.property('background').that.contains('rgb(255, 100, 0)'); 308 | expect(style).to.have.property('backgroundPosition').that.equals('10px 10px'); 309 | expect(style).to.have.property('backgroundSize', 'cover'); 310 | expect(style).to.have.property('padding', '5px'); 311 | expect(style).to.have.property('top', '100px'); 312 | expect(style).to.have.property('left', '100%'); 313 | 314 | root = render(( 315 |
    test
    316 | ), scratch, root); 317 | 318 | expect(root).to.have.deep.property('style.cssText').that.equals('color: rgb(0, 255, 255);'); 319 | 320 | root = render(( 321 |
    test
    322 | ), scratch, root); 323 | 324 | expect(root).to.have.deep.property('style.cssText').that.equals('display: inline;'); 325 | 326 | root = render(( 327 |
    test
    328 | ), scratch, root); 329 | 330 | expect(root).to.have.deep.property('style.cssText').that.equals('background-color: rgb(0, 255, 255);'); 331 | }); 332 | 333 | it('should serialize class/className', () => { 334 | render(
    , scratch); 346 | 347 | let { className } = scratch.childNodes[0]; 348 | expect(className).to.be.a.string; 349 | expect(className.split(' ')) 350 | .to.include.members(['yes1', 'yes2', 'yes3', 'yes4', 'yes5']) 351 | .and.not.include.members(['no1', 'no2', 'no3', 'no4', 'no5']); 352 | }); 353 | 354 | it('should support dangerouslySetInnerHTML', () => { 355 | let html = 'foo & bar'; 356 | let root = render(
    , scratch); 357 | 358 | expect(scratch.firstChild, 'set').to.have.property('innerHTML', html); 359 | expect(scratch.innerHTML).to.equal('
    '+html+'
    '); 360 | 361 | root = render(
    ab
    , scratch, root); 362 | 363 | expect(scratch, 'unset').to.have.property('innerHTML', `
    ab
    `); 364 | 365 | render(
    , scratch, root); 366 | 367 | expect(scratch.innerHTML, 're-set').to.equal('
    '+html+'
    '); 368 | }); 369 | 370 | it( 'should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => { 371 | class Thing extends Component { 372 | constructor(props, context) { 373 | super(props, context); 374 | this.state.html = this.props.html; 375 | } 376 | render(props, { html }) { 377 | return html ?
    :
    ; 378 | } 379 | } 380 | 381 | let thing; 382 | 383 | render( thing=c } html="test" />, scratch); 384 | 385 | expect(scratch.innerHTML).to.equal('
    test
    '); 386 | 387 | thing.setState({ html: false }); 388 | thing.forceUpdate(); 389 | 390 | expect(scratch.innerHTML).to.equal('
    '); 391 | 392 | thing.setState({ html: 'test' }); 393 | thing.forceUpdate(); 394 | 395 | expect(scratch.innerHTML).to.equal('
    test
    '); 396 | }); 397 | 398 | it('should hydrate with dangerouslySetInnerHTML', () => { 399 | let html = 'foo & bar'; 400 | scratch.innerHTML = `
    ${html}
    `; 401 | render(
    , scratch, scratch.lastChild); 402 | 403 | expect(scratch.firstChild).to.have.property('innerHTML', html); 404 | expect(scratch.innerHTML).to.equal(`
    ${html}
    `); 405 | }); 406 | 407 | it('should reconcile mutated DOM attributes', () => { 408 | let check = p => render(, scratch, scratch.lastChild), 409 | value = () => scratch.lastChild.checked, 410 | setValue = p => scratch.lastChild.checked = p; 411 | check(true); 412 | expect(value()).to.equal(true); 413 | check(false); 414 | expect(value()).to.equal(false); 415 | check(true); 416 | expect(value()).to.equal(true); 417 | setValue(true); 418 | check(false); 419 | expect(value()).to.equal(false); 420 | setValue(false); 421 | check(true); 422 | expect(value()).to.equal(true); 423 | }); 424 | 425 | it('should ignore props.children if children are manually specified', () => { 426 | expect( 427 |
    c
    428 | ).to.eql( 429 |
    c
    430 | ); 431 | }); 432 | 433 | it('should reorder child pairs', () => { 434 | let root = render(( 435 |
    436 | a 437 | b 438 |
    439 | ), scratch, root); 440 | 441 | let a = scratch.firstChild.firstChild; 442 | let b = scratch.firstChild.lastChild; 443 | 444 | expect(a).to.have.property('nodeName', 'A'); 445 | expect(b).to.have.property('nodeName', 'B'); 446 | 447 | root = render(( 448 |
    449 | b 450 | a 451 |
    452 | ), scratch, root); 453 | 454 | expect(scratch.firstChild.firstChild).to.have.property('nodeName', 'B'); 455 | expect(scratch.firstChild.lastChild).to.have.property('nodeName', 'A'); 456 | expect(scratch.firstChild.firstChild).to.equal(b); 457 | expect(scratch.firstChild.lastChild).to.equal(a); 458 | }); 459 | 460 | it('should skip non-preact elements', () => { 461 | class Foo extends Component { 462 | render() { 463 | let alt = this.props.alt || this.state.alt || this.alt; 464 | let c = [ 465 | foo, 466 | { alt?'alt':'bar' } 467 | ]; 468 | if (alt) c.reverse(); 469 | return
    {c}
    ; 470 | } 471 | } 472 | 473 | let comp; 474 | let root = render( comp = c } />, scratch, root); 475 | 476 | let c = document.createElement('c'); 477 | c.textContent = 'baz'; 478 | comp.base.appendChild(c); 479 | 480 | let b = document.createElement('b'); 481 | b.textContent = 'bat'; 482 | comp.base.appendChild(b); 483 | 484 | expect(scratch.firstChild.children, 'append').to.have.length(4); 485 | 486 | comp.forceUpdate(); 487 | 488 | expect(scratch.firstChild.children, 'forceUpdate').to.have.length(4); 489 | expect(scratch.innerHTML, 'forceUpdate').to.equal(`
    foobarbazbat
    `); 490 | 491 | comp.alt = true; 492 | comp.forceUpdate(); 493 | 494 | expect(scratch.firstChild.children, 'forceUpdate alt').to.have.length(4); 495 | expect(scratch.innerHTML, 'forceUpdate alt').to.equal(`
    altfoobazbat
    `); 496 | 497 | // Re-rendering from the root is non-destructive if the root was a previous render: 498 | comp.alt = false; 499 | root = render( comp = c } />, scratch, root); 500 | 501 | expect(scratch.firstChild.children, 'root re-render').to.have.length(4); 502 | expect(scratch.innerHTML, 'root re-render').to.equal(`
    foobarbazbat
    `); 503 | 504 | comp.alt = true; 505 | root = render( comp = c } />, scratch, root); 506 | 507 | expect(scratch.firstChild.children, 'root re-render 2').to.have.length(4); 508 | expect(scratch.innerHTML, 'root re-render 2').to.equal(`
    altfoobazbat
    `); 509 | 510 | root = render(
    comp = c } />
    , scratch, root); 511 | 512 | expect(scratch.firstChild.children, 'root re-render changed').to.have.length(3); 513 | expect(scratch.innerHTML, 'root re-render changed').to.equal(`
    foobar
    bazbat
    `); 514 | }); 515 | 516 | // Discussion: https://github.com/developit/preact/issues/287 517 | ('HTMLDataListElement' in window ? it : xit)('should allow to pass through as an attribute', () => { 518 | render(( 519 |
    520 | 521 | 522 | 523 | 524 | 525 | 526 |
    527 | ), scratch); 528 | 529 | let html = scratch.firstElementChild.firstElementChild.outerHTML; 530 | expect(sortAttributes(html)).to.equal(sortAttributes('')); 531 | }); 532 | }); 533 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Preact 3 | 4 | 5 | **Preact is a fast, `3kB` alternative to React, with the same ES2015 API.** 6 | 7 | Preact retains a large amount of compatibility with React, but only the modern ([ES6 Classes] and [stateless functional components](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components)) interfaces. 8 | As one would expect coming from React, Components are simple building blocks for composing a User Interface. 9 | 10 | ### :information_desk_person: Full documentation is available at the [Preact Website ➞](https://preactjs.com) 11 | 12 | [![CDNJS](https://img.shields.io/cdnjs/v/preact.svg)](https://cdnjs.com/libraries/preact) 13 | [![npm](https://img.shields.io/npm/v/preact.svg)](http://npm.im/preact) 14 | [![travis](https://travis-ci.org/developit/preact.svg?branch=master)](https://travis-ci.org/developit/preact) 15 | [![coveralls](https://img.shields.io/coveralls/developit/preact/master.svg)](https://coveralls.io/github/developit/preact) 16 | [![Preact Slack Community](https://preact-slack.now.sh/badge.svg)](https://preact-slack.now.sh) 17 | [![OpenCollective Backers](https://opencollective.com/preact/backers/badge.svg)](#backers) 18 | [![OpenCollective Sponsors](https://opencollective.com/preact/sponsors/badge.svg)](#sponsors) 19 | 20 | Preact supports modern browsers and IE9+. The chart below shows test status for `master`: 21 | 22 | [![Browsers](https://saucelabs.com/browser-matrix/preact.svg)](https://saucelabs.com/u/preact) 23 | 24 | 25 | --- 26 | 27 | 28 | ## Demos 29 | 30 | - [**ESBench**](http://esbench.com) is built using Preact. 31 | - [**Nectarine.rocks**](http://nectarine.rocks) _([Github Project](https://github.com/developit/nectarine))_ :peach: 32 | - [**Documentation Viewer**](https://documentation-viewer.firebaseapp.com) _([Github Project](https://github.com/developit/documentation-viewer))_ 33 | - [**TodoMVC**](https://preact-todomvc.surge.sh) _([Github Project](https://github.com/developit/preact-todomvc))_ 34 | - [**Hacker News Minimal**](https://developit.github.io/hn_minimal/) _([Github Project](https://github.com/developit/hn_minimal))_ 35 | - [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([Github Project](https://github.com/developit/preact-boilerplate))_ :zap: 36 | - [**Preact Offline Starter**](https://preact-starter.now.sh) _([Github Project](https://github.com/lukeed/preact-starter))_ :100: 37 | - [**Preact PWA**](https://preact-pwa.appspot.com/) _([Github Project](https://github.com/ezekielchentnik/preact-pwa))_ :hamburger: 38 | - [**Preact Mobx Starter**](https://awaw00.github.io/preact-mobx-starter/) _([Github Project](https://github.com/awaw00/preact-mobx-starter))_ :sunny: 39 | - [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star: 40 | - [**Flickr Browser**](http://codepen.io/developit/full/VvMZwK/) (@ CodePen) 41 | - [**Animating Text**](http://codepen.io/developit/full/LpNOdm/) (@ CodePen) 42 | - [**60FPS Rainbow Spiral**](http://codepen.io/developit/full/xGoagz/) (@ CodePen) 43 | - [**Simple Clock**](http://jsfiddle.net/developit/u9m5x0L7/embedded/result,js/) (@ JSFiddle) 44 | - [**3D + ThreeJS**](http://codepen.io/developit/pen/PPMNjd?editors=0010) (@ CodePen) 45 | - [**Stock Ticker**](http://codepen.io/developit/pen/wMYoBb?editors=0010) (@ CodePen) 46 | - [**Create your Own!**](https://jsfiddle.net/developit/rs6zrh5f/embedded/result/) (@ JSFiddle) 47 | - [**Preact Coffeescript**](https://github.com/crisward/preact-coffee) 48 | - [**GuriVR**](https://gurivr.com) _([Github Project](https://github.com/opennewslabs/guri-vr))_ 49 | - [**V2EX Preact**](https://github.com/yanni4night/v2ex-preact) 50 | - [**BigWebQuiz**](https://bigwebquiz.com/) _([Github Project](https://github.com/jakearchibald/big-web-quiz))_ 51 | 52 | ## Libraries & Add-ons 53 | 54 | - :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact *([full example](http://git.io/preact-compat-example))* 55 | - :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering. 56 | - :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components 57 | - :bookmark_tabs: [**preact-markup**](https://git.io/preact-markup): Render HTML & Custom Elements as JSX & Components 58 | - :satellite: [**preact-portal**](https://git.io/preact-portal): Render Preact components into (a) SPACE :milky_way: 59 | - :pencil: [**preact-richtextarea**](https://git.io/preact-richtextarea): Simple HTML editor component 60 | - :bookmark: [**preact-token-input**](https://github.com/developit/preact-token-input): Text field that tokenizes input, for things like tags 61 | - :card_index: [**preact-virtual-list**](https://github.com/developit/preact-virtual-list): Easily render lists with millions of rows ([demo](https://jsfiddle.net/developit/qqan9pdo/)) 62 | - :repeat: [**preact-cycle**](https://git.io/preact-cycle): Functional-reactive paradigm for Preact 63 | - :triangular_ruler: [**preact-layout**](https://download.github.io/preact-layout/): Small and simple layout library 64 | - :thought_balloon: [**preact-socrates**](https://github.com/matthewmueller/preact-socrates): Preact plugin for [Socrates](http://github.com/matthewmueller/socrates) 65 | - :rowboat: [**preact-flyd**](https://github.com/xialvjun/preact-flyd): Use [flyd](https://github.com/paldepind/flyd) FRP streams in Preact + JSX 66 | - :speech_balloon: [**preact-i18nline**](https://github.com/download/preact-i18nline): Integrates the ecosystem around [i18n-js](https://github.com/everydayhero/i18n-js) with Preact via [i18nline](https://github.com/download/i18nline). 67 | - :metal: [**preact-mui**](https://git.io/v1aVO): The MUI CSS Preact library. 68 | - :white_square_button: [**preact-mdl**](https://git.io/preact-mdl): Use [MDL](https://getmdl.io) as Preact components 69 | - :rocket: [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com) 70 | - :microscope: [**preact-jsx-chai**](https://git.io/preact-jsx-chai): JSX assertion testing _(no DOM, right in Node)_ 71 | - :tophat: [**preact-classless-component**](https://github.com/ld0rman/preact-classless-component): create preact components without the class keyword 72 | - :hammer: [**preact-hyperscript**](https://github.com/queckezz/preact-hyperscript): Hyperscript-like syntax for creating elements 73 | - :white_check_mark: [**shallow-compare**](https://github.com/tkh44/shallow-compare): simplified `shouldComponentUpdate` helper. 74 | - :shaved_ice: [**preact-codemod**](https://github.com/vutran/preact-codemod): Transform your React code to Preact. 75 | 76 | ## Getting Started 77 | 78 | > :information_desk_person: You [don't _have_ to use ES2015 to use Preact](https://github.com/developit/preact-without-babel)... but you should. 79 | 80 | The following guide assumes you have some sort of ES2015 build set up using babel and/or webpack/browserify/gulp/grunt/etc. If you don't, start with [preact-boilerplate] or a [CodePen Template](http://codepen.io/developit/pen/pgaROe?editors=0010). 81 | 82 | 83 | ### Import what you need 84 | 85 | The `preact` module provides both named and default exports, so you can either import everything under a namespace of your choosing, or just what you need as locals: 86 | 87 | ##### Named: 88 | 89 | ```js 90 | import { h, render, Component } from 'preact'; 91 | 92 | // Tell Babel to transform JSX into h() calls: 93 | /** @jsx h */ 94 | ``` 95 | 96 | ##### Default: 97 | 98 | ```js 99 | import preact from 'preact'; 100 | 101 | // Tell Babel to transform JSX into preact.h() calls: 102 | /** @jsx preact.h */ 103 | ``` 104 | 105 | > Named imports work well for highly structured applications, whereas the default import is quick and never needs to be updated when using different parts of the library. 106 | > 107 | > Instead of declaring the `@jsx` pragma in your code, it's best to configure it globally in a `.babelrc`: 108 | > 109 | > **For Babel 5 and prior:** 110 | > 111 | > ```json 112 | > { "jsxPragma": "h" } 113 | > ``` 114 | > 115 | > **For Babel 6:** 116 | > 117 | > ```json 118 | > { 119 | > "plugins": [ 120 | > ["transform-react-jsx", { "pragma":"h" }] 121 | > ] 122 | > } 123 | > ``` 124 | 125 | 126 | ### Rendering JSX 127 | 128 | Out of the box, Preact provides an `h()` function that turns your JSX into Virtual DOM elements _([here's how](http://jasonformat.com/wtf-is-jsx))_. It also provides a `render()` function that creates a DOM tree from that Virtual DOM. 129 | 130 | To render some JSX, just import those two functions and use them like so: 131 | 132 | ```js 133 | import { h, render } from 'preact'; 134 | 135 | render(( 136 |
    137 | Hello, world! 138 | 139 |
    140 | ), document.body); 141 | ``` 142 | 143 | This should seem pretty straightforward if you've used hyperscript or one of its many friends. If you're not, the short of it is that the h function import gets used in the final, transpiled code as a drop in replacement for React.createElement, and so needs to be imported even if you don't explicitly use it in the code you write. Also note that if you're the kind of person who likes writing your React code in "pure JavaScript" (you know who you are) you will need to use h(...) wherever you would otherwise use React.createElement. 144 | 145 | Rendering hyperscript with a virtual DOM is pointless, though. We want to render components and have them updated when data changes - that's where the power of virtual DOM diffing shines. :star2: 146 | 147 | 148 | ### Components 149 | 150 | Preact exports a generic `Component` class, which can be extended to build encapsulated, self-updating pieces of a User Interface. Components support all of the standard React [lifecycle methods], like `shouldComponentUpdate()` and `componentWillReceiveProps()`. Providing specific implementations of these methods is the preferred mechanism for controlling _when_ and _how_ components update. 151 | 152 | Components also have a `render()` method, but unlike React this method is passed `(props, state)` as arguments. This provides an ergonomic means to destructure `props` and `state` into local variables to be referenced from JSX. 153 | 154 | Let's take a look at a very simple `Clock` component, which shows the current time. 155 | 156 | ```js 157 | import { h, render, Component } from 'preact'; 158 | 159 | class Clock extends Component { 160 | render() { 161 | let time = new Date().toLocaleTimeString(); 162 | return { time }; 163 | } 164 | } 165 | 166 | // render an instance of Clock into : 167 | render(, document.body); 168 | ``` 169 | 170 | 171 | That's great. Running this produces the following HTML DOM structure: 172 | 173 | ```html 174 | 10:28:57 PM 175 | ``` 176 | 177 | In order to have the clock's time update every second, we need to know when `` gets mounted to the DOM. _If you've used HTML5 Custom Elements, this is similar to the `attachedCallback` and `detachedCallback` lifecycle methods._ Preact invokes the following lifecycle methods if they are defined for a Component: 178 | 179 | | Lifecycle method | When it gets called | 180 | |-----------------------------|--------------------------------------------------| 181 | | `componentWillMount` | before the component gets mounted to the DOM | 182 | | `componentDidMount` | after the component gets mounted to the DOM | 183 | | `componentWillUnmount` | prior to removal from the DOM | 184 | | `componentDidUnmount` | after removal from the DOM | 185 | | `componentWillReceiveProps` | before new props get accepted | 186 | | `shouldComponentUpdate` | before `render()`. Return `false` to skip render | 187 | | `componentWillUpdate` | before `render()` | 188 | | `componentDidUpdate` | after `render()` | 189 | 190 | 191 | 192 | So, we want to have a 1-second timer start once the Component gets added to the DOM, and stop if it is removed. We'll create the timer and store a reference to it in `componentDidMount`, and stop the timer in `componentWillUnmount`. On each timer tick, we'll update the component's `state` object with a new time value. Doing this will automatically re-render the component. 193 | 194 | ```js 195 | import { h, render, Component } from 'preact'; 196 | 197 | class Clock extends Component { 198 | constructor() { 199 | super(); 200 | // set initial time: 201 | this.state.time = Date.now(); 202 | } 203 | 204 | componentDidMount() { 205 | // update time every second 206 | this.timer = setInterval(() => { 207 | this.setState({ time: Date.now() }); 208 | }, 1000); 209 | } 210 | 211 | componentWillUnmount() { 212 | // stop when not renderable 213 | clearInterval(this.timer); 214 | } 215 | 216 | render(props, state) { 217 | let time = new Date(state.time).toLocaleTimeString(); 218 | return { time }; 219 | } 220 | } 221 | 222 | // render an instance of Clock into : 223 | render(, document.body); 224 | ``` 225 | 226 | Now we have [a ticking clock](http://jsfiddle.net/developit/u9m5x0L7/embedded/result,js/)! 227 | 228 | 229 | ### Props & State 230 | 231 | The concept (and nomenclature) for `props` and `state` is the same as in React. `props` are passed to a component by defining attributes in JSX, `state` is internal state. Changing either triggers a re-render, though by default Preact re-renders Components asynchronously for `state` changes and synchronously for `props` changes. You can tell Preact to render `prop` changes asynchronously by setting `options.syncComponentUpdates` to `false`. 232 | 233 | 234 | --- 235 | 236 | 237 | ## Linked State 238 | 239 | One area Preact takes a little further than React is in optimizing state changes. A common pattern in ES2015 React code is to use Arrow functions within a `render()` method in order to update state in response to events. Creating functions enclosed in a scope on every render is inefficient and forces the garbage collector to do more work than is necessary. 240 | 241 | One solution to this is to bind component methods declaratively. 242 | Here is an example using [decko](http://git.io/decko): 243 | 244 | ```js 245 | class Foo extends Component { 246 | @bind 247 | updateText(e) { 248 | this.setState({ text: e.target.value }); 249 | } 250 | render({ }, { text }) { 251 | return ; 252 | } 253 | } 254 | ``` 255 | 256 | While this achieves much better runtime performance, it's still a lot of unnecessary code to wire up state to UI. 257 | 258 | Fortunately there is a solution, in the form of `linkState()`. Calling `component.linkState('text')` returns a function that accepts an Event and uses it's associated value to update the given property in your component's state. Calls to linkState() with the same state property are cached, so there is no performance penalty. Here is the previous example rewritten using _Linked State_: 259 | 260 | ```js 261 | class Foo extends Component { 262 | render({ }, { text }) { 263 | return ; 264 | } 265 | } 266 | ``` 267 | 268 | Simple and effective. It handles linking state from any input type, or an optional second parameter can be used to explicitly provide a keypath to the new state value. 269 | 270 | 271 | ## Examples 272 | 273 | Here is a somewhat verbose Preact `` component: 274 | 275 | ```js 276 | class Link extends Component { 277 | render(props, state) { 278 | return { props.children }; 279 | } 280 | } 281 | ``` 282 | 283 | Since this is ES6/ES2015, we can further simplify: 284 | 285 | ```js 286 | class Link extends Component { 287 | render({ href, children }) { 288 | return ; 289 | } 290 | } 291 | 292 | // or, for wide-open props support: 293 | class Link extends Component { 294 | render(props) { 295 | return ; 296 | } 297 | } 298 | 299 | // or, as a stateless functional component: 300 | const Link = ({ children, ...props }) => ( 301 | { children } 302 | ); 303 | ``` 304 | 305 | 306 | ## Extensions 307 | 308 | It is likely that some projects based on Preact would wish to extend Component with great new functionality. 309 | 310 | Perhaps automatic connection to stores for a Flux-like architecture, or mixed-in context bindings to make it feel more like `React.createClass()`. Just use ES2015 inheritance: 311 | 312 | ```js 313 | class BoundComponent extends Component { 314 | constructor(props) { 315 | super(props); 316 | this.bind(); 317 | } 318 | bind() { 319 | this.binds = {}; 320 | for (let i in this) { 321 | this.binds[i] = this[i].bind(this); 322 | } 323 | } 324 | } 325 | 326 | // example usage 327 | class Link extends BoundComponent { 328 | click() { 329 | open(this.href); 330 | } 331 | render() { 332 | let { click } = this.binds; 333 | return { children }; 334 | } 335 | } 336 | ``` 337 | 338 | 339 | The possibilities are pretty endless here. You could even add support for rudimentary mixins: 340 | 341 | ```js 342 | class MixedComponent extends Component { 343 | constructor() { 344 | super(); 345 | (this.mixins || []).forEach( m => Object.assign(this, m) ); 346 | } 347 | } 348 | ``` 349 | 350 | ## Developer Tools 351 | 352 | You can inspect and modify the state of your Preact UI components at runtime using the 353 | [React Developer Tools](https://github.com/facebook/react-devtools) browser extension. 354 | 355 | 1. Install the [React Developer Tools](https://github.com/facebook/react-devtools) extension 356 | 2. Import the "preact/devtools" module in your app 357 | 3. Reload and go to the 'React' tab in the browser's development tools 358 | 359 | 360 | ```js 361 | import { h, Component, render } from 'preact'; 362 | 363 | // Enable devtools. You can reduce the size of your app by only including this 364 | // module in development builds. eg. In Webpack, wrap this with an `if (module.hot) {...}` 365 | // check. 366 | require('preact/devtools'); 367 | ``` 368 | 369 | 370 | 371 | ## Backers 372 | Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/preact#backer)] 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | ## Sponsors 407 | Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/preact#sponsor)] 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | ## License 441 | 442 | MIT 443 | 444 | 445 | 446 | [![Preact](http://i.imgur.com/YqCHvEW.gif)](https://preactjs.com) 447 | 448 | 449 | 450 | [ES6 Classes]: https://facebook.github.io/react/docs/reusable-components.html#es6-classes 451 | [hyperscript]: https://github.com/dominictarr/hyperscript 452 | [preact-boilerplate]: https://github.com/developit/preact-boilerplate 453 | [lifecycle methods]: https://facebook.github.io/react/docs/component-specs.html 454 | --------------------------------------------------------------------------------