diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..354c4e0
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,8 @@
+{
+ "sourceMap": "inline",
+ "presets": ["es2015-loose", "stage-0", "react"],
+ "plugins": [
+ "transform-object-rest-spread",
+ ["transform-react-jsx", { "pragma":"h" }]
+ ]
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9c378bd
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[{package.json,.*rc,*.yml}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..44477dd
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+*.md
+*.conf.js
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..c56fcd1
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,55 @@
+{
+ "parser": "babel-eslint",
+ "extends": "eslint:recommended",
+ "env": {
+ "browser": true,
+ "mocha": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaFeatures": {
+ "modules": true,
+ "jsx": true
+ }
+ },
+ "rules": {
+ "no-empty": 0,
+ "no-console": 0,
+ "no-unused-vars": [0, { "varsIgnorePattern": "^h$" }],
+ "no-cond-assign": 1,
+ "semi": 2,
+ "camelcase": 0,
+ "comma-style": 2,
+ "comma-dangle": [2, "never"],
+ "indent": [2, "tab", {"SwitchCase": 1}],
+ "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
+ "no-trailing-spaces": [2, { "skipBlankLines": true }],
+ "max-nested-callbacks": [2, 3],
+ "no-eval": 2,
+ "no-implied-eval": 2,
+ "no-new-func": 2,
+ "guard-for-in": 2,
+ "eqeqeq": 2,
+ "no-else-return": 2,
+ "no-redeclare": 2,
+ "no-dupe-keys": 2,
+ "radix": 2,
+ "strict": [2, "never"],
+ "no-shadow": 0,
+ "no-delete-var": 2,
+ "no-undef-init": 2,
+ "no-shadow-restricted-names": 2,
+ "handle-callback-err": 0,
+ "no-lonely-if": 2,
+ "keyword-spacing": 2,
+ "constructor-super": 2,
+ "no-this-before-super": 2,
+ "no-dupe-class-members": 2,
+ "no-const-assign": 2,
+ "prefer-spread": 2,
+ "no-useless-concat": 2,
+ "no-var": 2,
+ "object-shorthand": 2,
+ "prefer-arrow-callback": 2
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72e9b45
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/dist
+/node_modules
+/npm-debug.log
+.DS_Store
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..4c2095a
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1 @@
+.eslintrc
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..8524235
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - 4
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..38d8969
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Jason Miller
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..19dc455
--- /dev/null
+++ b/README.md
@@ -0,0 +1,84 @@
+# preact-portal
+
+[![NPM](https://img.shields.io/npm/v/preact-portal.svg?style=flat)](https://www.npmjs.org/package/preact-portal)
+[![travis-ci](https://travis-ci.org/developit/preact-portal.svg?branch=master)](https://travis-ci.org/developit/preact-portal)
+
+> Render [Preact] components somewhere in [a different] space.
+
+Use this if you have a component that needs to render children into some other place in the DOM.
+
+An example of this would be modal dialogs, where you may need to render `` into `
`.
+
+
+**[JSFiddle Demo](http://jsfiddle.net/developit/f1jmxtvg/)**
+
+
+---
+
+
+## Installation
+
+Via npm:
+
+`npm install --save preact-portal`
+
+
+
+## Usage
+
+```js
+import { h, Component, render } from 'preact';
+import Portal from 'preact-portal';
+
+class Thumbnail extends Component {
+ open = () => this.setState({ open:true });
+ close = () => this.setState({ open:false });
+
+ render({ url }, { open }) {
+ return (
+
+
+
+ { open ? (
+
+
+
+ ) : null }
+
+ );
+ }
+}
+
+render(, document.body);
+```
+
+
+---
+
+
+Or, wrap up a very common case into a simple high order function:
+
+```js
+const Popup = ({ open, into="body", children }) => (
+ open ? { children } : null
+);
+
+// Example: show popup on error.
+class Form extends Component {
+ render({}, { error }) {
+ return (
+
+ );
+ }
+}
+```
+
+
+[preact]: https://github.com/developit/preact
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..aa60ee6
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,42 @@
+var path = require('path');
+
+module.exports = function(config) {
+ config.set({
+ frameworks: ['mocha', 'chai-sinon'],
+ reporters: ['mocha'],
+
+ browsers: ['PhantomJS'],
+
+ files: [
+ 'test/**/*.js'
+ ],
+
+ preprocessors: {
+ 'test/**/*.js': ['webpack'],
+ 'src/**/*.js': ['webpack'],
+ '**/*.js': ['sourcemap']
+ },
+
+ webpack: {
+ module: {
+ loaders: [
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ loader: 'babel'
+ }
+ ]
+ },
+ resolve: {
+ modulesDirectories: [__dirname, 'node_modules'],
+ alias: {
+ src: __dirname+'/src'
+ }
+ }
+ },
+
+ webpackMiddleware: {
+ noInfo: true
+ }
+ });
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..839e582
--- /dev/null
+++ b/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "preact-portal",
+ "amdName": "preactPortal",
+ "version": "1.0.0",
+ "description": "Render Preact components somewhere in [a different] space.",
+ "main": "dist/preact-portal.js",
+ "minified:main": "dist/preact-portal.min.js",
+ "jsnext:main": "src/preact-portal.js",
+ "scripts": {
+ "build": "npm-run-all transpile minify size",
+ "transpile": "rollup -c rollup.config.js",
+ "minify": "uglifyjs $npm_package_main -cm -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map",
+ "size": "echo \"gzip size: $(gzip-size $npm_package_minified_main | pretty-bytes)\"",
+ "test": "npm-run-all lint build test:karma",
+ "lint": "eslint {src,test}",
+ "test:karma": "karma start --single-run",
+ "prepublish": "npm-run-all build test",
+ "release": "npm run -s build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish",
+ "start": "node server.js"
+ },
+ "keywords": [
+ "preact",
+ "react",
+ "portals"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/developit/preact-portal.git"
+ },
+ "author": "Jason Miller ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/developit/preact-portal/issues"
+ },
+ "homepage": "https://github.com/developit/preact-portal",
+ "devDependencies": {
+ "babel-core": "^6.6.4",
+ "babel-eslint": "^5.0.0",
+ "babel-loader": "^6.2.3",
+ "babel-plugin-transform-class-properties": "^6.5.2",
+ "babel-plugin-transform-object-rest-spread": "^6.6.4",
+ "babel-plugin-transform-react-jsx": "^6.6.5",
+ "babel-preset-es2015": "^6.6.0",
+ "babel-preset-es2015-loose": "^7.0.0",
+ "babel-preset-es2015-loose-rollup": "^7.0.0",
+ "babel-preset-es2015-minimal": "^1.1.0",
+ "babel-preset-es2015-minimal-rollup": "^1.1.0",
+ "babel-preset-react": "^6.5.0",
+ "babel-preset-stage-0": "^6.5.0",
+ "chai": "^3.5.0",
+ "eslint": "~2.2.0",
+ "gzip-size-cli": "^1.0.0",
+ "karma": "^0.13.21",
+ "karma-chai": "^0.1.0",
+ "karma-chai-sinon": "^0.1.5",
+ "karma-mocha": "^0.2.2",
+ "karma-mocha-reporter": "^1.2.1",
+ "karma-phantomjs-launcher": "^1.0.0",
+ "karma-sourcemap-loader": "^0.3.7",
+ "karma-webpack": "^1.7.0",
+ "mkdirp": "^0.5.1",
+ "mocha": "^2.4.5",
+ "npm-run-all": "^1.5.1",
+ "phantomjs-prebuilt": "^2.1.4",
+ "preact": "^4.1.1",
+ "pretty-bytes-cli": "^1.0.0",
+ "rollup": "^0.25.4",
+ "rollup-plugin-babel": "^2.4.0",
+ "rollup-plugin-commonjs": "^2.2.1",
+ "rollup-plugin-node-resolve": "^1.4.0",
+ "sinon": "^1.17.2",
+ "sinon-chai": "^2.8.0",
+ "uglify-js": "^2.6.2",
+ "webpack": "^1.12.14"
+ },
+ "peerDependencies": {
+ "preact": "*"
+ },
+ "directories": {
+ "test": "test"
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..80b90d1
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,40 @@
+import path from 'path';
+import fs from 'fs';
+import babel from 'rollup-plugin-babel';
+import nodeResolve from 'rollup-plugin-node-resolve';
+import commonjs from 'rollup-plugin-commonjs';
+
+let babelrc = JSON.parse(fs.readFileSync('./.babelrc'));
+let pkg = JSON.parse(fs.readFileSync('./package.json'));
+let external = Object.keys(pkg.peerDependencies || {}).concat(Object.keys(pkg.dependencies || {}));
+
+export default {
+ entry: pkg['jsnext:main'],
+ dest: pkg.main,
+ sourceMap: path.resolve(pkg.main),
+ moduleName: pkg.amdName,
+ format: 'umd',
+ external,
+ plugins: [
+ babel({
+ babelrc: false,
+ comments: false,
+ exclude: 'node_modules/**',
+ presets: [
+ 'es2015-minimal-rollup'
+ ].concat(babelrc.presets.slice(1)),
+ plugins: require('babel-preset-es2015-minimal-rollup').plugins.concat([
+ ['transform-react-jsx', { pragma:'h' }]
+ ])
+ }),
+ nodeResolve({
+ jsnext: true,
+ main: true,
+ skip: external
+ }),
+ commonjs({
+ include: 'node_modules/**',
+ exclude: '**/*.css'
+ })
+ ]
+};
diff --git a/src/preact-portal.js b/src/preact-portal.js
new file mode 100644
index 0000000..a31efae
--- /dev/null
+++ b/src/preact-portal.js
@@ -0,0 +1,53 @@
+import { h, Component, render } from 'preact';
+
+/** Redirect rendering of descendants into the given CSS selector.
+ * @example
+ *
+ * I am rendered into document.body
+ *
+ */
+export default class Portal extends Component {
+ componentDidUpdate(props) {
+ for (let i in props) {
+ if (props[i]!==this.props[i]) {
+ return this.renderLayer();
+ }
+ }
+ }
+
+ componentDidMount() {
+ this.renderLayer();
+ }
+
+ componentWillUnmount() {
+ this.renderLayer(false);
+ }
+
+ findNode(node) {
+ return typeof node==='string' ? document.querySelector(node) : node;
+ }
+
+ renderLayer(show=true) {
+ // clean up old node if moving bases:
+ if (this.props.into!==this.intoPointer) {
+ this.intoPointer = this.props.into;
+ if (this.into && this.remote) {
+ render(, this.into, this.remote);
+ }
+ this.into = this.findNode(this.props.into);
+ }
+
+ this.remote = render((
+ { show && this.props.children || null }
+ ), this.into, this.remote);
+ }
+}
+
+
+// high-order component that renders its first child if it exists.
+// used as a conditional rendering proxy.
+class PortalProxy extends Component {
+ render({ children }) {
+ return children && children[0] || null;
+ }
+}
diff --git a/test/preact-portal.js b/test/preact-portal.js
new file mode 100644
index 0000000..6f0507a
--- /dev/null
+++ b/test/preact-portal.js
@@ -0,0 +1,39 @@
+import { h, Component, render } from 'preact';
+import Portal from '../src/preact-portal';
+
+/*global sinon,expect*/
+
+describe('preact-portal', () => {
+ let scratch;
+
+ before( () => {
+ scratch = document.createElement('div');
+ (document.body || document.documentElement).appendChild(scratch);
+ });
+
+ beforeEach( () => {
+ scratch.innerHTML = '';
+ });
+
+ after( () => {
+ scratch.parentNode.removeChild(scratch);
+ scratch = null;
+ });
+
+ it('should have tests', () => {
+ expect(Portal).to.be.a('function');
+ });
+
+ it('should render into target', () => {
+ let foo = document.createElement('div');
+ foo.setAttribute('id', 'foo');
+ scratch.appendChild(foo);
+
+ let base = document.createElement('div');
+ scratch.appendChild(base);
+
+ render(hello
, base);
+
+ expect(foo).to.have.property('innerHTML', 'hello
');
+ });
+});