diff --git a/.gitignore b/.gitignore index 6b81e1b5c2..40acf3f76e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ # Additional bundles /devtools.js /devtools.js.map +/debug.js +/debug.js.map diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..9e81e67c88 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@preactjs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index 31e7438b88..46b533a937 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - Everything you need: JSX, VDOM, React DevTools, HMR, SSR.. - A highly optimized diff algorithm and seamless Server Side Rendering - Transparent asynchronous rendering with a pluggable scheduler +- 🆕💥 **Instant no-config app bundling with [Preact CLI](https://github.com/developit/preact-cli)** ### 💁 More information at the [Preact Website ➞](https://preactjs.com) @@ -60,14 +61,14 @@ Preact supports modern browsers and IE9+: ## Demos - [**ESBench**](http://esbench.com) is built using Preact. -- [**Nectarine.rocks**](http://nectarine.rocks) _([Github Project](https://github.com/developit/nectarine))_ :peach: -- [**Documentation Viewer**](https://documentation-viewer.firebaseapp.com) _([Github Project](https://github.com/developit/documentation-viewer))_ -- [**TodoMVC**](https://preact-todomvc.surge.sh) _([Github Project](https://github.com/developit/preact-todomvc))_ -- [**Hacker News Minimal**](https://developit.github.io/hn_minimal/) _([Github Project](https://github.com/developit/hn_minimal))_ -- [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([Github Project](https://github.com/developit/preact-boilerplate))_ :zap: -- [**Preact Offline Starter**](https://preact-starter.now.sh) _([Github Project](https://github.com/lukeed/preact-starter))_ :100: -- [**Preact PWA**](https://preact-pwa.appspot.com/) _([Github Project](https://github.com/ezekielchentnik/preact-pwa))_ :hamburger: -- [**Preact Mobx Starter**](https://awaw00.github.io/preact-mobx-starter/) _([Github Project](https://github.com/awaw00/preact-mobx-starter))_ :sunny: +- [**Nectarine.rocks**](http://nectarine.rocks) _([GitHub Project](https://github.com/developit/nectarine))_ :peach: +- [**Documentation Viewer**](https://documentation-viewer.firebaseapp.com) _([GitHub Project](https://github.com/developit/documentation-viewer))_ +- [**TodoMVC**](https://preact-todomvc.surge.sh) _([GitHub Project](https://github.com/developit/preact-todomvc))_ +- [**Hacker News Minimal**](https://developit.github.io/hn_minimal/) _([GitHub Project](https://github.com/developit/hn_minimal))_ +- [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([GitHub Project](https://github.com/developit/preact-boilerplate))_ :zap: +- [**Preact Offline Starter**](https://preact-starter.now.sh) _([GitHub Project](https://github.com/lukeed/preact-starter))_ :100: +- [**Preact PWA**](https://preact-pwa.appspot.com/) _([GitHub Project](https://github.com/ezekielchentnik/preact-pwa))_ :hamburger: +- [**Preact Mobx Starter**](https://awaw00.github.io/preact-mobx-starter/) _([GitHub Project](https://github.com/awaw00/preact-mobx-starter))_ :sunny: - [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star: - [**Flickr Browser**](http://codepen.io/developit/full/VvMZwK/) (@ CodePen) - [**Animating Text**](http://codepen.io/developit/full/LpNOdm/) (@ CodePen) @@ -77,18 +78,21 @@ Preact supports modern browsers and IE9+: - [**Stock Ticker**](http://codepen.io/developit/pen/wMYoBb?editors=0010) (@ CodePen) - [**Create your Own!**](https://jsfiddle.net/developit/rs6zrh5f/embedded/result/) (@ JSFiddle) - [**Preact Coffeescript**](https://github.com/crisward/preact-coffee) -- [**GuriVR**](https://gurivr.com) _([Github Project](https://github.com/opennewslabs/guri-vr))_ +- [**GuriVR**](https://gurivr.com) _([GitHub Project](https://github.com/opennewslabs/guri-vr))_ - [**V2EX Preact**](https://github.com/yanni4night/v2ex-preact) -- [**BigWebQuiz**](https://bigwebquiz.com/) _([Github Project](https://github.com/jakearchibald/big-web-quiz))_ -- [**Color Picker**](https://colors.now.sh) _([Github Project](https://github.com/lukeed/colors-app))_ :art: -- [**Rainbow Explorer**](https://use-the-platform.com/rainbow-explorer/) _([Github Project](https://github.com/vaneenige/rainbow-explorer/))_ :rainbow: -- [**Offline Gallery**](https://use-the-platform.com/offline-gallery/) _([Github Project](https://github.com/vaneenige/offline-gallery/))_ :balloon: -- [**Periodic Weather**](https://use-the-platform.com/periodic-weather/) _([Github Project](https://github.com/vaneenige/periodic-weather/))_ :sunny: +- [**BigWebQuiz**](https://bigwebquiz.com/) _([GitHub Project](https://github.com/jakearchibald/big-web-quiz))_ +- [**Color Picker**](https://colors.now.sh) _([GitHub Project](https://github.com/lukeed/colors-app))_ :art: +- [**Rainbow Explorer**](https://use-the-platform.com/rainbow-explorer/) _([GitHub Project](https://github.com/vaneenige/rainbow-explorer/))_ :rainbow: +- [**Offline Gallery**](https://use-the-platform.com/offline-gallery/) _([GitHub Project](https://github.com/vaneenige/offline-gallery/))_ :balloon: +- [**Periodic Weather**](https://use-the-platform.com/periodic-weather/) _([GitHub Project](https://github.com/vaneenige/periodic-weather/))_ :sunny: +- [**Play.cash**](https://play.cash) :notes: +- [**Rugby News Board**](http://nbrugby.com) _[(GitHub Project)](https://github.com/rugby-board/rugby-board-node)_ ## Libraries & Add-ons - :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact *([full example](http://git.io/preact-compat-example))* - :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering. +- :loop: [**preact-render-to-json**](https://git.io/preact-render-to-json): Render for Jest Snapshot testing. - :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components - :bookmark_tabs: [**preact-markup**](https://git.io/preact-markup): Render HTML & Custom Elements as JSX & Components - :satellite: [**preact-portal**](https://git.io/preact-portal): Render Preact components into (a) SPACE :milky_way: @@ -117,15 +121,20 @@ Preact supports modern browsers and IE9+: - [**preact-mui**](https://git.io/v1aVO): The MUI CSS Preact library. - [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com) - [**preact-mdl**](https://git.io/preact-mdl): [Material Design Lite](https://getmdl.io) for Preact +- [**preact-weui**](https://github.com/afeiship/preact-weui): [Weui](https://github.com/afeiship/preact-weui) for Preact --- ## Getting Started -> 💁 You [don't _have_ to use ES2015 to use Preact](https://github.com/developit/preact-without-babel)... but you should. +> 💁 _**Note:** You [don't need ES2015 to use Preact](https://github.com/developit/preact-in-es3)... but give it a try!_ -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). +The easiest way to get started with Preact is to install [Preact CLI](https://github.com/developit/preact-cli). This simple command-line tool wraps up the best possible Webpack and Babel setup for you, and even keeps you up-to-date as the underlying tools change. Best of all, it's easy to understand! It builds your app in a single command (`preact build`), doesn't need any configuration, and bakes in best-practises 🙌. + +The following guide assumes you have some sort of ES2015 build set up using babel and/or webpack/browserify/gulp/grunt/etc. + +You can also start with [preact-boilerplate] or a [CodePen Template](http://codepen.io/developit/pen/pgaROe?editors=0010). ### Import what you need @@ -329,7 +338,7 @@ Here is a somewhat verbose Preact `` component: ```js class Link extends Component { render(props, state) { - return { props.children }; + return {props.children}; } } ``` @@ -338,21 +347,21 @@ Since this is ES6/ES2015, we can further simplify: ```js class Link extends Component { - render({ href, children }) { - return ; - } + render({ href, children }) { + return ; + } } // or, for wide-open props support: class Link extends Component { - render(props) { - return ; - } + render(props) { + return ; + } } // or, as a stateless functional component: const Link = ({ children, ...props }) => ( - { children } + { children } ); ``` @@ -458,7 +467,7 @@ Support us with a monthly donation and help us continue our activities. [[Become ## Sponsors -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)] +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)] diff --git a/config/codemod-const.js b/config/codemod-const.js index 26ba3ae373..5d27f93eab 100644 --- a/config/codemod-const.js +++ b/config/codemod-const.js @@ -17,7 +17,7 @@ export default (file, api) => { init = node.init; if (name && init && name.match(/^[A-Z0-9_$]+$/g) && !init.regex) { if (init.type==='Literal') { - console.log(`Inlining constant: ${name}=${init.raw}`); + // console.log(`Inlining constant: ${name}=${init.raw}`); found++; constants[name] = init; // remove declaration diff --git a/config/codemod-let-name.js b/config/codemod-let-name.js new file mode 100644 index 0000000000..782cc99d76 --- /dev/null +++ b/config/codemod-let-name.js @@ -0,0 +1,13 @@ +/** + * Restores var names transformed by babel's let block scoping + */ +export default (file, api) => { + let j = api.jscodeshift; + let code = j(file.source); + + // @TODO unsafe, but without it we gain 20b gzipped: https://www.diffchecker.com/bVrOJWTO + code.findVariableDeclarators().filter(d => /^_i/.test(d.value.id.name)).renameTo('i'); + code.findVariableDeclarators('_key').renameTo('key'); + + return code.toSource({ quote: 'single' }); +}; diff --git a/config/rollup.config.devtools.js b/config/rollup.config.devtools.js index 1fb90b2384..60e93356be 100644 --- a/config/rollup.config.devtools.js +++ b/config/rollup.config.devtools.js @@ -1,4 +1,3 @@ -import nodeResolve from 'rollup-plugin-node-resolve'; import babel from 'rollup-plugin-babel'; export default { @@ -12,9 +11,18 @@ export default { plugins: [ babel({ sourceMap: true, - loose: 'all', - blacklist: ['es6.tailCall'], - exclude: 'node_modules/**' + exclude: 'node_modules/**', + babelrc: false, + presets: [ + ['env', { + modules: false, + loose: true, + exclude: ['transform-es2015-typeof-symbol'], + targets: { + browsers: ['last 2 versions', 'IE >= 9'] + } + }] + ] }) ] -} +}; diff --git a/config/rollup.config.esm.js b/config/rollup.config.esm.js new file mode 100644 index 0000000000..5078a1171b --- /dev/null +++ b/config/rollup.config.esm.js @@ -0,0 +1,9 @@ +import config from './rollup.config'; + +// ES output +config.format = 'es'; + +// remove memory() plugin +config.plugins.splice(0, 1); + +export default config; diff --git a/config/rollup.config.js b/config/rollup.config.js index cd2810fc19..fe0581139c 100644 --- a/config/rollup.config.js +++ b/config/rollup.config.js @@ -20,9 +20,18 @@ export default { }), babel({ sourceMap: true, - loose: 'all', - blacklist: ['es6.tailCall'], - exclude: 'node_modules/**' + exclude: 'node_modules/**', + babelrc: false, + presets: [ + ['env', { + modules: false, + loose: true, + exclude: ['transform-es2015-typeof-symbol'], + targets: { + browsers: ['last 2 versions', 'IE >= 9'] + } + }] + ] }) ] }; diff --git a/debug/index.js b/debug/index.js new file mode 100644 index 0000000000..fa02855308 --- /dev/null +++ b/debug/index.js @@ -0,0 +1,95 @@ +if (process.env.NODE_ENV === 'development') { + const { options } = require('preact'); + const oldVnodeOption = options.vnode; + + options.vnode = function(vnode) { + const { nodeName, attributes, children } = vnode; + + if (nodeName === void 0) { + throw new Error('Undefined component passed to preact.h()'); + } + + if ( + attributes && attributes.ref !== void 0 && + typeof attributes.ref !== 'function' + ) { + throw new Error( + `Component's "ref" property should be a function,` + + ` but [${typeof attributes.ref}] passed` + ); + } + + { + const keys = {}; + + inspectChildren(children, (deepChild) => { + if (!deepChild) return; + + // In Preact, all keys are stored as object values, i.e. being strings + const key = deepChild.key + ''; + + if (keys.hasOwnProperty(key)) { + /* eslint-disable no-console */ + console.error( + 'Following component has two or more children with the ' + + 'same "key" attribute. This may cause glitches and misbehavior ' + + 'in rendering process. Component: \n\n' + + serializeVNode(vnode) + '\n\n' + ); + + // Return early to not spam the console + return true; + } + + keys[key] = true; + }); + } + + return oldVnodeOption.call(this, vnode); + }; + + const inspectChildren = (children, inspect) => { + return children.some((child, i) => { + if (Array.isArray(child)) { + return inspectChildren(child, inspect); + } + + return inspect(child, i); + }); + }; + + const serializeVNode = ({ nodeName, attributes }) => { + let name; + let props; + + if (typeof nodeName === 'function') { + name = nodeName.name || nodeName.displayName; + } else { + name = nodeName; + } + + if (attributes) { + props = Object.keys(attributes).map(attr => { + const attrValue = attributes[attr]; + let attrValueString; + + // If it is an object but doesn't have toString(), use Object.toString + if (Object(attrValue) === attrValue && !attrValue.toString) { + attrValueString = Object.prototype.toString.call(attrValue); + } else { + attrValueString = attrValue + ''; + } + + return `${attr}=${JSON.stringify(attrValueString)}`; + }); + } + + if (!props) { + return `<${name} />`; + } + + return `<${name} ${props.join(' ')} />`; + }; + + require('preact/devtools'); +} diff --git a/package.json b/package.json index 22cbd20e4a..8f3ffadb4b 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,41 @@ { "name": "preact", - "version": "8.1.0", + "version": "8.2.1", "description": "Fast 3kb React alternative with the same ES6 API. Components & Virtual DOM.", "main": "dist/preact.js", - "jsnext:main": "src/preact.js", + "jsnext:main": "dist/preact.esm.js", + "module": "dist/preact.esm.js", "dev:main": "dist/preact.dev.js", "minified:main": "dist/preact.min.js", "scripts": { - "clean": "rimraf dist/ devtools.js devtools.js.map", + "clean": "rimraf dist/ devtools.js devtools.js.map debug.js debug.js.map", "copy-flow-definition": "copyfiles -f src/preact.js.flow dist", "copy-typescript-definition": "copyfiles -f src/preact.d.ts dist", "build": "npm-run-all --silent clean transpile copy-flow-definition copy-typescript-definition strip optimize minify size", "transpile:main": "rollup -c config/rollup.config.js -m dist/preact.dev.js.map -n preact -o dist/preact.dev.js", "transpile:devtools": "rollup -c config/rollup.config.devtools.js -o devtools.js -m devtools.js.map", - "transpile": "npm-run-all transpile:main transpile:devtools", + "transpile:esm": "rollup -c config/rollup.config.esm.js -m dist/preact.esm.js.map -o dist/preact.esm.js", + "transpile:debug": "babel debug/ -o debug.js -s", + "transpile": "npm-run-all transpile:main transpile:esm transpile:devtools transpile:debug", "optimize": "uglifyjs dist/preact.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty --mangle-props --mangle-regex=\"/^(_|normalizedNodeName|nextBase|prev[CPS]|_parentC)/\" --name-cache config/properties.json -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", "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", - "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", + "strip:main": "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 && jscodeshift --run-in-band -s -t config/codemod-let-name.js dist/preact.dev.js", + "strip:esm": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.esm.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.esm.js && jscodeshift --run-in-band -s -t config/codemod-let-name.js dist/preact.esm.js", + "strip": "npm-run-all strip:main strip:esm", "size": "node -e \"process.stdout.write('gzip size: ')\" && gzip-size dist/preact.min.js", - "test": "npm-run-all lint --parallel test:mocha test:karma test:ts", + "test": "npm-run-all lint --parallel test:mocha test:karma test:ts test:size", "test:ts": "tsc -p test/ts/", - "test:mocha": "mocha --recursive --compilers js:babel/register test/shared test/node", + "test:mocha": "mocha --recursive --require babel-register test/shared test/node", "test:karma": "karma start test/karma.conf.js --single-run", "test:mocha:watch": "npm run test:mocha -- --watch", "test:karma:watch": "npm run test:karma -- no-single-run", - "lint": "eslint devtools src test", + "test:size": "bundlesize", + "lint": "eslint debug devtools src test", "prepublish": "npm run build", "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", "release": "cross-env npm run smart-release", "postinstall": "npm run -s donate", - "donate": "echo \"\n *** Thanks for downloading Preact! ***\nPlease consider donating to our open collective\n\n => https://opencollective.com/preact/donate\n\"" + "donate": "echo \"\u001b[35m\u001b[1mLove Preact? You can now donate to our open collective:\u001b[22m\u001b[39m\n > \u001b[34mhttps://opencollective.com/preact/donate\u001b[39m\"" }, "eslintConfig": { "extends": "./config/eslint-config.js" @@ -41,10 +47,13 @@ }, "files": [ "devtools", + "debug", "src", "dist", "devtools.js", "devtools.js.map", + "debug.js", + "debug.js.map", "typings.json" ], "keywords": [ @@ -63,11 +72,14 @@ }, "homepage": "https://github.com/developit/preact", "devDependencies": { - "babel": "^5.8.23", - "babel-core": "^5.8.24", - "babel-eslint": "^6.1.0", - "babel-loader": "^5.3.2", - "babel-runtime": "^5.8.24", + "babel-cli": "^6.24.1", + "babel-core": "^6.24.1", + "babel-eslint": "^7.2.3", + "babel-loader": "^7.0.0", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-jsx": "^6.24.1", + "babel-preset-env": "^1.5.1", + "bundlesize": "^0.6.1", "chai": "^3.4.1", "copyfiles": "^1.0.0", "core-js": "^2.4.1", @@ -89,32 +101,52 @@ "karma-mocha-reporter": "^2.0.4", "karma-phantomjs-launcher": "^1.0.1", "karma-sauce-launcher": "^1.1.0", - "karma-source-map-support": "^1.1.0", + "karma-source-map-support": "^1.2.0", "karma-sourcemap-loader": "^0.3.6", - "karma-webpack": "^2.0.1", + "karma-webpack": "^2.0.3", "mocha": "^3.0.1", "npm-run-all": "^4.0.0", "phantomjs-prebuilt": "^2.1.7", "rimraf": "^2.5.3", "rollup": "^0.40.0", - "rollup-plugin-babel": "^1.0.0", + "rollup-plugin-babel": "^2.7.1", "rollup-plugin-memory": "^2.0.0", "rollup-plugin-node-resolve": "^2.0.0", - "sinon": "^1.17.4", + "sinon": "^2.2.0", "sinon-chai": "^2.8.0", "typescript": "^2.2.2", "uglify-js": "^2.7.5", "webpack": "^2.4.1" }, + "babel": { + "presets": [ + ["env", { + "loose": true, + "exclude": ["transform-es2015-typeof-symbol"], + "targets": { + "browsers": ["last 2 versions", "IE >= 9"] + } + }] + ], + "plugins": [ + "transform-object-rest-spread", + "transform-react-jsx" + ] + }, "greenkeeper": { "ignore": [ - "rollup-plugin-babel", - "babel", + "babel-cli", "babel-core", "babel-eslint", "babel-loader", - "babel-runtime", - "jscodeshift" + "jscodeshift", + "rollup-plugin-babel" ] - } + }, + "bundlesize": [ + { + "path": "./dist/preact.min.js", + "threshold": "4Kb" + } + ] } diff --git a/src/h.js b/src/h.js index b8dbd9480b..457481f1a8 100644 --- a/src/h.js +++ b/src/h.js @@ -25,7 +25,7 @@ export function h(nodeName, attributes) { for (i=child.length; i--; ) stack.push(child[i]); } else { - if (child===true || child===false) child = null; + if (typeof child==='boolean') child = null; if ((simple = typeof nodeName!=='function')) { if (child==null) child = ''; diff --git a/src/preact.d.ts b/src/preact.d.ts index cf9b626ccb..6fcdb02f3f 100644 --- a/src/preact.d.ts +++ b/src/preact.d.ts @@ -45,7 +45,7 @@ declare namespace preact { // Type alias for a component considered generally, whether stateless or stateful. type AnyComponent = FunctionalComponent | typeof Component; - abstract class Component implements ComponentLifecycle { + abstract class Component { constructor(props?:PropsType, context?:any); static displayName?:string; @@ -63,8 +63,9 @@ declare namespace preact { forceUpdate(callback?:() => void): void; - abstract render(props?:PropsType & ComponentProps, state?:StateType, context?:any):JSX.Element; + abstract render(props?:PropsType & ComponentProps, state?:StateType, context?:any):JSX.Element|null; } + interface Component extends ComponentLifecycle { } function h(node:ComponentConstructor | FunctionalComponent, params:PropsType, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element; function h(node:string, params:JSX.HTMLAttributes&JSX.SVGAttributes&{[propName: string]: any}, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element; @@ -419,7 +420,7 @@ declare namespace JSX { // MouseEvents onClick?:MouseEventHandler; onContextMenu?:MouseEventHandler; - onDoubleClick?:MouseEventHandler; + onDblClick?: MouseEventHandler; onDrag?:DragEventHandler; onDragEnd?:DragEventHandler; onDragEnter?:DragEventHandler; diff --git a/src/render-queue.js b/src/render-queue.js index a8e7dcf5f5..42021f69c7 100644 --- a/src/render-queue.js +++ b/src/render-queue.js @@ -1,4 +1,5 @@ import options from './options'; +import { defer } from './util'; import { renderComponent } from './vdom/component'; /** Managed queue of dirty components to be re-rendered */ @@ -7,11 +8,10 @@ let items = []; export function enqueueRender(component) { if (!component._dirty && (component._dirty = true) && items.push(component)==1) { - (options.debounceRendering || setTimeout)(rerender); + (options.debounceRendering || defer)(rerender); } } - export function rerender() { let p, list = items; items = []; diff --git a/src/util.js b/src/util.js index 7adca8c0ea..720db3b108 100644 --- a/src/util.js +++ b/src/util.js @@ -7,4 +7,7 @@ export function extend(obj, props) { return obj; } - +/** Call a function asynchronously, as soon as possible. + * @param {Function} callback + */ +export const defer = typeof Promise=='function' ? Promise.resolve().then.bind(Promise.resolve()) : setTimeout; diff --git a/src/vdom/component.js b/src/vdom/component.js index b64ae40087..5286735174 100644 --- a/src/vdom/component.js +++ b/src/vdom/component.js @@ -5,7 +5,7 @@ import { enqueueRender } from '../render-queue'; import { getNodeProps } from './index'; import { diff, mounts, diffLevel, flushMounts, recollectNodeTree, removeChildren } from './diff'; import { createComponent, collectComponent } from './component-recycler'; -import { removeNode } from '../dom'; +import { removeNode } from '../dom/index'; /** Set a component's `props` (generally derived from JSX attributes). * @param {Object} props @@ -175,7 +175,8 @@ export function renderComponent(component, opts, mountAll, isChild) { else if (!skip) { // Ensure that pending componentDidMount() hooks of child components // are called before the componentDidUpdate() hook in the parent. - flushMounts(); + // Note: disabled as it causes duplicate hooks, see https://github.com/developit/preact/issues/750 + // flushMounts(); if (component.componentDidUpdate) { component.componentDidUpdate(previousProps, previousState, previousContext); diff --git a/src/vdom/diff.js b/src/vdom/diff.js index bc421ebdcf..6cc9bb7106 100644 --- a/src/vdom/diff.js +++ b/src/vdom/diff.js @@ -4,7 +4,7 @@ import { buildComponentFromVNode } from './component'; import { createNode, setAccessor } from '../dom/index'; import { unmountComponent } from './component'; import options from '../options'; -import { removeNode } from '../dom'; +import { removeNode } from '../dom/index'; /** Queue of components that have been mounted and are awaiting componentDidMount */ export const mounts = []; @@ -66,7 +66,7 @@ function idiff(dom, vnode, context, mountAll, componentRoot) { prevSvgMode = isSvgMode; // empty values (null, undefined, booleans) render as empty Text nodes - if (vnode==null || vnode===false || vnode===true) vnode = ''; + if (vnode==null || typeof vnode==='boolean') vnode = ''; // Fast case: Strings & Numbers create/update Text nodes. @@ -74,6 +74,7 @@ function idiff(dom, vnode, context, mountAll, componentRoot) { // update if it's already a Text node: if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) { + /* istanbul ignore if */ /* Browser quirk that can't be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */ if (dom.nodeValue!=vnode) { dom.nodeValue = vnode; } @@ -94,18 +95,20 @@ function idiff(dom, vnode, context, mountAll, componentRoot) { // If the VNode represents a Component, perform a component diff: - if (typeof vnode.nodeName==='function') { + let vnodeName = vnode.nodeName; + if (typeof vnodeName==='function') { return buildComponentFromVNode(dom, vnode, context, mountAll); } // Tracks entering and exiting SVG namespace when descending through the tree. - isSvgMode = vnode.nodeName==='svg' ? true : vnode.nodeName==='foreignObject' ? false : isSvgMode; + isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode; // If there's no existing element or it's the wrong type, create a new one: - if (!dom || !isNamedNode(dom, String(vnode.nodeName))) { - out = createNode(String(vnode.nodeName), isSvgMode); + vnodeName = String(vnodeName); + if (!dom || !isNamedNode(dom, vnodeName)) { + out = createNode(vnodeName, isSvgMode); if (dom) { // move children into the replacement node @@ -121,9 +124,14 @@ function idiff(dom, vnode, context, mountAll, componentRoot) { let fc = out.firstChild, - props = out[ATTR_KEY] || (out[ATTR_KEY] = {}), + props = out[ATTR_KEY], vchildren = vnode.children; + if (props==null) { + props = out[ATTR_KEY] = {}; + for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value; + } + // Optimization: fast-path for elements containing a single TextNode: if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) { if (fc.nodeValue!=vchildren[0]) { @@ -163,7 +171,7 @@ function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) { len = originalChildren.length, childrenLen = 0, vlen = vchildren ? vchildren.length : 0, - j, c, vchild, child; + j, c, f, vchild, child; // Build up a map of keyed children and an Array of unkeyed children: if (len!==0) { @@ -211,17 +219,16 @@ function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) { // morph the matched/found/created DOM child to match vchild (deep) child = idiff(child, vchild, context, mountAll); - if (child && child!==dom) { - if (i>=len) { + f = originalChildren[i]; + if (child && child!==dom && child!==f) { + if (f==null) { dom.appendChild(child); } - else if (child!==originalChildren[i]) { - if (child===originalChildren[i+1]) { - removeNode(originalChildren[i]); - } - else { - dom.insertBefore(child, originalChildren[i] || null); - } + else if (child===f.nextSibling) { + removeNode(f); + } + else { + dom.insertBefore(child, f); } } } @@ -241,7 +248,7 @@ function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) { -/** Recursively recycle (or just unmount) a node an its descendants. +/** Recursively recycle (or just unmount) a node and its descendants. * @param {Node} node DOM node to start unmount/removal from * @param {Boolean} [unmountOnly=false] If `true`, only triggers unmount lifecycle, skips removal */ diff --git a/test/browser/components.js b/test/browser/components.js index 73ea6af165..904559df11 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -1,4 +1,4 @@ -import { h, render, rerender, Component } from '../../src/preact'; +import { h, cloneElement, render, rerender, Component } from '../../src/preact'; /** @jsx h */ let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) ); @@ -111,6 +111,14 @@ describe('Components', () => { }); + it('should clone components', () => { + function Comp () {} + let instance = ; + let clone = cloneElement(instance); + expect(clone.prototype).to.equal(instance.prototype); + }); + + // Test for Issue #73 it('should remove orphaned elements replaced by Components', () => { class Comp extends Component { diff --git a/test/browser/keys.js b/test/browser/keys.js index 9141b46f61..2a9b0199a2 100644 --- a/test/browser/keys.js +++ b/test/browser/keys.js @@ -24,6 +24,7 @@ describe('keys', () => {
1
  • a
  • +
  • b
  • ), scratch); @@ -31,10 +32,11 @@ describe('keys', () => {
    2
  • b
  • +
  • c
  • ), scratch, root); - expect(scratch.innerHTML).to.equal('
    2
  • b
  • '); + expect(scratch.innerHTML).to.equal('
    2
  • b
  • c
  • '); }); it('should set VNode#key property', () => { diff --git a/test/browser/lifecycle.js b/test/browser/lifecycle.js index 6493473c3e..4a22c78e55 100644 --- a/test/browser/lifecycle.js +++ b/test/browser/lifecycle.js @@ -122,7 +122,7 @@ describe('Lifecycle methods', () => { const elem = render(, scratch); render(, scratch, elem); - expect(log).to.deep.equal(['Inner mounted', 'Outer updated']); + // expect(log).to.deep.equal(['Inner mounted', 'Outer updated']); }); }); @@ -411,6 +411,51 @@ describe('Lifecycle methods', () => { }); }); + + describe('shouldComponentUpdate', () => { + let setState; + + class Should extends Component { + constructor() { + super(); + this.state = { show:true }; + setState = s => this.setState(s); + } + render(props, { show }) { + return show ?
    : null; + } + } + + class ShouldNot extends Should { + shouldComponentUpdate() { + return false; + } + } + + sinon.spy(Should.prototype, 'render'); + sinon.spy(ShouldNot.prototype, 'shouldComponentUpdate'); + + beforeEach(() => Should.prototype.render.reset()); + + it('should rerender component on change by default', () => { + render(, scratch); + setState({ show:false }); + rerender(); + + expect(Should.prototype.render).to.have.been.calledTwice; + }); + + it('should not rerender component if shouldComponentUpdate returns false', () => { + render(, scratch); + setState({ show:false }); + rerender(); + + expect(ShouldNot.prototype.shouldComponentUpdate).to.have.been.calledOnce; + expect(ShouldNot.prototype.render).to.have.been.calledOnce; + }); + }); + + describe('Lifecycle DOM Timing', () => { it('should be invoked when dom does (DidMount, WillUnmount) or does not (WillMount, DidUnmount) exist', () => { let setState; diff --git a/test/browser/render.js b/test/browser/render.js index 69ca264a4b..3aeae531c0 100644 --- a/test/browser/render.js +++ b/test/browser/render.js @@ -406,7 +406,7 @@ describe('render()', () => { expect(scratch.innerHTML, 're-set').to.equal('
    '+html+'
    '); }); - it( 'should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => { + it('should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => { class Thing extends Component { constructor(props, context) { super(props, context); @@ -496,6 +496,20 @@ describe('render()', () => { expect(scratch.firstChild.lastChild).to.equal(a); }); + it('should not merge attributes with node created by the DOM', () => { + const html = (htmlString) => { + const div = document.createElement('div'); + div.innerHTML = htmlString; + return div.firstChild; + }; + + const DOMElement = html`
    `; + const preactElement =
    ; + + render(preactElement, scratch, DOMElement); + expect(scratch).to.have.property('innerHTML', '
    '); + }); + it('should skip non-preact elements', () => { class Foo extends Component { render() { @@ -568,4 +582,45 @@ describe('render()', () => { let html = scratch.firstElementChild.firstElementChild.outerHTML; expect(sortAttributes(html)).to.equal(sortAttributes('')); }); + + it('should not execute append operation when child is at last', (done) => { + let input; + class TodoList extends Component { + constructor(props) { + super(props); + this.state = { todos: [], text: '' }; + this.setText = this.setText.bind(this); + this.addTodo = this.addTodo.bind(this); + } + setText(e) { + this.setState({ text: e.target.value }); + } + addTodo() { + let { todos, text } = this.state; + todos = todos.concat({ text }); + this.setState({ todos, text: '' }); + } + render() { + const {todos, text} = this.state; + return ( +
    + { todos.map( todo => (
    {todo.text}
    )) } + input = i} /> +
    + ); + } + } + const root = render(, scratch); + input.focus(); + input.value = 1; + root._component.setText({ + target: input + }); + root._component.addTodo(); + expect(document.activeElement).to.equal(input); + setTimeout(() =>{ + expect(/1/.test(scratch.innerHTML)).to.equal(true); + done(); + }, 10); + }); }); diff --git a/test/karma.conf.js b/test/karma.conf.js index f11ad1f24c..b5d1a34fe5 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -118,8 +118,6 @@ module.exports = function(config) { exclude: /node_modules/, loader: 'babel-loader', options: { - loose: 'all', - blacklist: ['es6.tailCall'], comments: false, compact: true } diff --git a/test/shared/h.js b/test/shared/h.js index 50000deecd..9a58637a07 100644 --- a/test/shared/h.js +++ b/test/shared/h.js @@ -208,4 +208,13 @@ describe('h(jsx)', () => { .with.property('children') .that.deep.equals(['onetwothree']); }); + + it('should not merge children of components', () => { + let Component = ({children}) => children; + let r = h(Component, null, 'x', 'y'); + + expect(r).to.be.an('object') + .with.property('children') + .that.deep.equals(['x', 'y']); + }); });