├── .babelrc
├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── dist
├── preact-habitat.es.js
├── preact-habitat.es.js.map
├── preact-habitat.js
├── preact-habitat.js.map
├── preact-habitat.umd.js
└── preact-habitat.umd.js.map
├── docs
├── artwork.png
├── artwork_2.png
├── artworkv3.gif
└── readmev2.md
├── examples
└── login-form
│ ├── .babelrc
│ ├── .editorconfig
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── signin
│ │ │ ├── index.js
│ │ │ └── style.scss
│ │ └── widget.js
│ └── index.js
│ ├── tools
│ ├── build.cli.js
│ ├── lint.cli.js
│ ├── start.cli.js
│ └── test.cli.js
│ └── webpack.config.babel.js
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
├── habitat.d.ts
├── index.js
├── lib.js
└── test
├── habitat.test.js
└── lib.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceMaps": true,
3 | "presets": [["es2015", { "loose": true }], "stage-0"],
4 | "plugins": [["transform-react-jsx", { "pragma": "h" }]]
5 | }
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = false
9 | insert_final_newline = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *~
3 | *.log
4 | /*.js.map
5 | .DS_Store
6 |
7 | Thumbs.db
8 | /node_modules
9 | /coverage
10 | .idea
11 | /.vscode
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - '6'
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 |
10 | install:
11 | - npm install preact@latest
12 | - npm install
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) [Zouhir Chahoud](https://zouhir.org)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
90 | ...
91 | ...
92 | ...
93 | ...
94 | ...
95 |
96 |
97 | ```
98 |
99 | ### Mount multiple widgets (developer specified divs)
100 | ```html
101 |
102 |
103 |
104 |
105 |
106 | ```
107 | ```js
108 | import habitat from 'preact-habitat';
109 | import WidgetAwesomeThree from './components/WidgetAwesome';
110 |
111 | let habitatThree = habitat(WidgetAwesomeTwo);
112 | habitatThree.render({ name: 'data-widget-here-please', value: 'widget-number-three', inline: false });
113 |
114 | ```
115 | ### Render Method API
116 |
117 | Render method accepts an *Object* and supports 3 properties
118 |
119 | - name: HTML tag attribute name, Srting, default `data-mount`.
120 | - value: HTML tag attribute value, Strinf, default null.
121 | - inline: Enable \ Disable inline mounting for the widget, Boolean.
122 |
123 |
124 | ### Prop Names Rules
125 | Now habitat allow you to pass props from HTML to your preact components, here are the rules:
126 |
127 | - *starts with* `data-prop-`
128 | - *all lower case* `data-prop-videoid` === `this.prop.videoid`
129 | - *add dashes for camelCase* 🐫 `data-prop-video-id` === `this.prop.videoId`
130 |
131 |
132 | ##

Thank You!, But..
133 |
134 | 1. Please make sure your widget size is reasonable, bloated and big size bundles make puppies sick 🐶 😔
135 |
136 | 2. Feel free to fork, contribute or give it a 🌟. Open an issue or [chat with me](https://twitter.com/_zouhir) if you have any questions.
137 |
138 |
139 | ## License
140 |
141 | [MIT](LICENSE) - Copyright (c) [Zouhir Chahoud](http://zouhir.org)
--------------------------------------------------------------------------------
/examples/login-form/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceMaps": true,
3 | "presets": [
4 | ["es2015", { "loose":true }],
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | ["transform-decorators-legacy"],
9 | ["transform-react-jsx", { "pragma": "h" }]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/login-form/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/examples/login-form/.gitignore:
--------------------------------------------------------------------------------
1 | # stupid files
2 | *.swp
3 | *~
4 | *.log
5 | /*.js.map
6 | .DS_Store
7 |
8 |
9 | node_modules/
10 |
11 | # ignore build
12 | build/
13 |
14 | # ignore test coverage
15 | coverage.data
16 | coverage/
17 | dev_modules/
18 |
--------------------------------------------------------------------------------
/examples/login-form/README.md:
--------------------------------------------------------------------------------
1 | # Preact Widgets Boilerplate
2 |
3 | > Sample repo to build small pluggable component widgets
4 |
5 | # Demos:
6 |
7 | - *Simple Login* 🔑 [link](https://preact-habitat-inline.netlify.com/)
8 |
9 | - *Youtube Players* ▶️ [link](https://preact-habitat-youtube.netlify.com/)
10 |
11 |
12 | ## License
13 |
14 | MIT
15 |
--------------------------------------------------------------------------------
/examples/login-form/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Light Weight Preact Widgets
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |

23 |
24 |
Preact Habitat
25 |
28 |
32 |
33 | Preact habitat is a 1kb module that will help you ship your Preact components to any world wide DOM page in a very easy and neat way.
34 |
35 |
Demo
36 |
37 |
38 |
39 | total: 18kb minified, 7kb gzipped
40 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Built with 💛 and Preact by Zouhir
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/examples/login-form/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "widgets-preact-boilerplate",
3 | "version": "0.9.0",
4 | "description": "Ready-to-go Preact starter project powered by webpack.",
5 | "scripts": {
6 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress",
7 | "start": "serve build -s -c 1 && node ./tools/start.cli.js",
8 | "prestart": "npm run build",
9 | "build": "cross-env NODE_ENV=production webpack -p --progress && node ./tools/build.cli.js",
10 | "prebuild": "mkdirp build",
11 | "test": "npm run lint && npm run -s test:karma && node ./tools/test.cli.js",
12 | "test:karma": "karma start test/karma.conf.js --single-run",
13 | "lint": "eslint ./src/ || true && node ./tools/lint.cli.js"
14 | },
15 | "keywords": [
16 | "preact",
17 | "boilerplate",
18 | "webpack"
19 | ],
20 | "license": "MIT",
21 | "author": "Jason Miller
",
22 | "devDependencies": {
23 | "autoprefixer": "^6.4.0",
24 | "babel": "^6.5.2",
25 | "babel-core": "^6.14.0",
26 | "babel-eslint": "^7.0.0",
27 | "babel-loader": "^6.2.5",
28 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
29 | "babel-plugin-transform-react-jsx": "^6.8.0",
30 | "babel-preset-es2015": "^6.14.0",
31 | "babel-preset-stage-0": "^6.5.0",
32 | "babel-register": "^6.14.0",
33 | "babel-runtime": "^6.11.6",
34 | "chai": "^3.5.0",
35 | "colors": "^1.1.2",
36 | "copy-webpack-plugin": "^4.0.1",
37 | "core-js": "^2.4.1",
38 | "cross-env": "^3.1.4",
39 | "css-loader": "^0.26.1",
40 | "eslint": "^3.0.1",
41 | "extract-text-webpack-plugin": "^1.0.1",
42 | "file-loader": "^0.9.0",
43 | "html-webpack-plugin": "^2.22.0",
44 | "isparta-loader": "^2.0.0",
45 | "json-loader": "^0.5.4",
46 | "karma": "^1.0.0",
47 | "karma-chai": "^0.1.0",
48 | "karma-chai-sinon": "^0.1.5",
49 | "karma-coverage": "^1.1.1",
50 | "karma-mocha": "^1.0.1",
51 | "karma-mocha-reporter": "^2.1.0",
52 | "karma-phantomjs-launcher": "^1.0.2",
53 | "karma-sourcemap-loader": "^0.3.7",
54 | "karma-webpack": "^1.8.0",
55 | "less": "^2.7.1",
56 | "less-loader": "^2.2.3",
57 | "mkdirp": "^0.5.1",
58 | "mocha": "^3.2.0",
59 | "ncp": "^2.0.0",
60 | "node-sass": "^4.2.0",
61 | "phantomjs-prebuilt": "^2.1.12",
62 | "postcss-loader": "^1.2.1",
63 | "raw-loader": "^0.5.1",
64 | "replace-bundle-webpack-plugin": "^1.0.0",
65 | "sass-loader": "^4.1.1",
66 | "sinon": "^1.17.7",
67 | "sinon-chai": "^2.8.0",
68 | "source-map-loader": "^0.1.6",
69 | "style-loader": "^0.13.0",
70 | "url-loader": "^0.5.7",
71 | "webpack": "^1.13.2",
72 | "webpack-dev-server": "^1.15.0"
73 | },
74 | "dependencies": {
75 | "preact": "^7.1.0",
76 | "preact-compat": "^3.0.0",
77 | "preact-habitat": "^3.0.2",
78 | "promise-polyfill": "^6.0.2",
79 | "proptypes": "^0.14.3",
80 | "serve": "^2.0.0"
81 | },
82 | "main": "webpack.config.babel.js",
83 | "directories": {
84 | "test": "test"
85 | },
86 | "repository": {
87 | "type": "git",
88 | "url": "git+https://github.com/zouhir/preact-widgets-boilerplate.git"
89 | },
90 | "bugs": {
91 | "url": "https://github.com/zouhir/preact-widgets-boilerplate/issues"
92 | },
93 | "homepage": "https://github.com/zouhir/preact-widgets-boilerplate#readme"
94 | }
95 |
--------------------------------------------------------------------------------
/examples/login-form/src/components/signin/index.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from 'preact';
2 | import style from './style';
3 |
4 | export default class SigninWidget extends Component {
5 | render() {
6 | return (
7 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/login-form/src/components/signin/style.scss:
--------------------------------------------------------------------------------
1 | // widgets colors variables
2 | $beige: #F4F1EA;
3 | $yellow: #FFBB41;
4 | $black: #37394C;
5 | $google-red: #EA4335;
6 | $facebook-blue: #3b5998;
7 | $white-sand: #FAF8F5;
8 |
9 | .signinWidget {
10 | * {
11 | box-sizing: border-box;
12 | }
13 | position: relative;
14 | border-radius: 6px;
15 | background: #FFF;
16 | box-shadow: 0 10px 20px 0 rgba(0,0,0,0.10);
17 | width: 100%;
18 | max-width: 500px;
19 | margin: 30px auto 30px auto;
20 | padding: 15px;
21 | font-family: sans-serif;
22 | hr {
23 | opacity: 0.2;
24 | margin: 40px auto;
25 | position: relative;
26 | &:before {
27 | content: 'OR';
28 | position: absolute;
29 | left: 50%;
30 | top: 50%;
31 | transform: translate(-50%, -50%);
32 | background: #FFF;
33 | padding: 0px 10px;
34 | }
35 | }
36 | h3 {
37 | font-family: sans-serif;
38 | color: $black;
39 | font-size: 18px;
40 | margin-bottom: 30px;
41 | }
42 | p {
43 | font-size: 14px;
44 | margin-bottom: 15px;
45 | line-height: 18px;
46 | }
47 | .colOne {
48 | width: 40%;
49 | padding-right: 20px;
50 | }
51 | .colTwo {
52 | width: 60%;
53 | }
54 | input[type="text"] , input[type="password"] {
55 | height: 40px;
56 | line-height: 38px;
57 | width: 100%;
58 | background: $white-sand;
59 | border: none;
60 | border-radius: 4px;
61 | margin-bottom: 20px;
62 | margin: 15px auto;
63 | display: block;
64 | font-size: 13px;
65 | font-weight: 100;
66 | padding: 0 12px;
67 | }
68 | }
69 |
70 | .basebtn {
71 | height: 40px;
72 | width: 100%;
73 | max-width: 230px;
74 | font-family: sans-serif;
75 | text-transform: uppercase;
76 | font-size: 12px;
77 | text-align: center;
78 | line-height: 40px;
79 | text-decoration: none;
80 | border-radius: 3px;
81 | color: #FFF;
82 | &.google {
83 | background: $google-red;
84 | margin: 15px auto;
85 | display: block;
86 | }
87 |
88 | &.facebook {
89 | background: $facebook-blue;
90 | margin: 15px auto;
91 | display: block;
92 | }
93 |
94 | &.default {
95 | margin: 0px 0 15px 0;
96 | display: block;
97 | }
98 | }
99 |
100 | .wrapper {
101 | width: 100%;
102 | max-width: 500px;
103 | margin: 10px auto;
104 | overflow: hidden;
105 | &:last-child{
106 | margin-bottom: 0px;
107 | }
108 | }
109 |
110 | .note {
111 | font-family: sans-serif;
112 | color: $black;
113 | font-size: 13px;
114 | a {
115 | color: $yellow;
116 | opacity: 1;
117 | }
118 | }
119 |
120 | .formFooter {
121 | font-family: sans-serif;
122 | color: $black;
123 | opacity: 1;
124 | font-size: 13px;
125 | display: block;
126 | width: 100%;
127 | margin-bottom: 15px;
128 | color: $yellow;
129 | }
130 |
--------------------------------------------------------------------------------
/examples/login-form/src/components/widget.js:
--------------------------------------------------------------------------------
1 | // theirs
2 | import { h, Component } from 'preact';
3 |
4 | // ours
5 | import SigninWidget from './signin';
6 |
7 | export default class Widget extends Component {
8 | state = {
9 | isAauthenticated: false
10 | };
11 | static defaultProps = {
12 | title: 'no-title!',
13 | message: 'default prop'
14 | };
15 | handleAuth = () => {
16 | this.setState({ isAauthenticated: true });
17 | };
18 | render() {
19 | let { isAauthenticated } = this.state;
20 | return (
21 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/login-form/src/index.js:
--------------------------------------------------------------------------------
1 | import habitat from 'preact-habitat';
2 | import Widget from './components/widget';
3 |
4 | function init() {
5 | let niceLogin = habitat(Widget);
6 | /**
7 | * option 1: render inline
8 | */
9 | niceLogin.render({
10 | inline: true,
11 | clean: false
12 | });
13 |
14 | /**
15 | * option 2: render in selector
16 | */
17 | // niceLogin.render({
18 | // selector: ".widget-container",
19 | // inline: false,
20 | // clean: false
21 | // });
22 |
23 | /**
24 | * option 3: render in cleinet specified
25 | */
26 | // niceLogin.render({
27 | // clientSpecified: true
28 | // inline: false,
29 | // clean: false
30 | // });
31 | }
32 |
33 | // in development, set up HMR:
34 | if (module.hot) {
35 | require('preact/devtools'); // enables React DevTools, be careful on IE
36 | module.hot.accept('./components/widget', () => requestAnimationFrame(init));
37 | }
38 |
39 | init();
40 |
--------------------------------------------------------------------------------
/examples/login-form/tools/build.cli.js:
--------------------------------------------------------------------------------
1 | require('colors');
2 |
3 | console.log('\n 📦 building task is finished... \n'.magenta);
4 |
--------------------------------------------------------------------------------
/examples/login-form/tools/lint.cli.js:
--------------------------------------------------------------------------------
1 | require('colors');
2 |
3 | console.log('\n ✨ task lint is finished... \n'.yellow);
4 |
--------------------------------------------------------------------------------
/examples/login-form/tools/start.cli.js:
--------------------------------------------------------------------------------
1 | require('colors');
2 |
3 | console.log('\n 🚀 server started and is running your production package! \n'.cyan);
4 |
--------------------------------------------------------------------------------
/examples/login-form/tools/test.cli.js:
--------------------------------------------------------------------------------
1 | require('colors');
2 |
3 | console.log('\n 🙏 testing is done \n'.bgGreen);
4 |
--------------------------------------------------------------------------------
/examples/login-form/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
3 | import autoprefixer from 'autoprefixer';
4 | import CopyWebpackPlugin from 'copy-webpack-plugin';
5 | import ReplacePlugin from 'replace-bundle-webpack-plugin';
6 | import path from 'path';
7 |
8 | const ENV = process.env.NODE_ENV || 'development';
9 |
10 | const CSS_MAPS = ENV !== 'production';
11 |
12 | module.exports = {
13 | context: path.resolve(__dirname, 'src'),
14 | entry: './index.js',
15 |
16 | output: {
17 | path: path.resolve(__dirname, 'build'),
18 | publicPath: '/',
19 | filename: 'bundle.js',
20 | libraryTarget: 'umd'
21 | },
22 |
23 | resolve: {
24 | extensions: ['', '.jsx', '.js', '.json', '.scss'],
25 | modulesDirectories: [
26 | path.resolve(__dirname, 'src/lib'),
27 | path.resolve(__dirname, 'node_modules'),
28 | 'node_modules'
29 | ],
30 | alias: {
31 | components: path.resolve(__dirname, 'src/components'), // used for tests
32 | style: path.resolve(__dirname, 'src/style'),
33 | react: 'preact-compat',
34 | 'react-dom': 'preact-compat'
35 | }
36 | },
37 |
38 | module: {
39 | loaders: [
40 | {
41 | test: /\.jsx?$/,
42 | exclude: /node_modules/,
43 | loader: 'babel'
44 | },
45 | {
46 | // Transform our own .(scss|css) files with PostCSS and CSS-modules
47 | test: /\.(scss|css)$/,
48 | include: [path.resolve(__dirname, 'src/components')],
49 | loader: [
50 | `style-loader?singleton`,
51 | `css-loader?modules&importLoaders=1&sourceMap=${CSS_MAPS}`,
52 | 'postcss-loader',
53 | `sass-loader?sourceMap=${CSS_MAPS}`
54 | ].join('!')
55 | },
56 | {
57 | test: /\.(scss|css)$/,
58 | exclude: [path.resolve(__dirname, 'src/components')],
59 | loader: [
60 | `style-loader?singleton`,
61 | `css?sourceMap=${CSS_MAPS}`,
62 | `postcss`,
63 | `sass?sourceMap=${CSS_MAPS}`
64 | ].join('!')
65 | },
66 | {
67 | test: /\.json$/,
68 | loader: 'json'
69 | },
70 | {
71 | test: /\.(xml|html|txt|md)$/,
72 | loader: 'raw'
73 | },
74 | {
75 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i,
76 | loader: ENV === 'production'
77 | ? 'file?name=[path][name]_[hash:base64:5].[ext]'
78 | : 'url'
79 | }
80 | ]
81 | },
82 |
83 | postcss: () => [autoprefixer({ browsers: 'last 2 versions' })],
84 |
85 | plugins: [
86 | new webpack.NoErrorsPlugin(),
87 | new webpack.DefinePlugin({
88 | 'process.env.NODE_ENV': JSON.stringify(ENV)
89 | })
90 | ].concat(
91 | ENV === 'production'
92 | ? [
93 | // strip out babel-helper invariant checks
94 | new ReplacePlugin([
95 | {
96 | // this is actually the property name https://github.com/kimhou/replace-bundle-webpack-plugin/issues/1
97 | partten: /throw\s+(new\s+)?[a-zA-Z]+Error\s*\(/g,
98 | replacement: () => 'return;('
99 | }
100 | ])
101 | ]
102 | : []
103 | ),
104 |
105 | stats: { colors: true },
106 |
107 | node: {
108 | global: true,
109 | process: false,
110 | Buffer: false,
111 | __filename: false,
112 | __dirname: false,
113 | setImmediate: false
114 | },
115 |
116 | devtool: ENV === 'production' ? 'source-map' : '',
117 |
118 | devServer: {
119 | port: process.env.PORT || 8080,
120 | host: 'localhost',
121 | colors: true,
122 | publicPath: '/build',
123 | contentBase: './',
124 | historyApiFallback: true,
125 | open: true
126 | }
127 | };
128 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "preact-habitat",
3 | "amdName": "preactHabitat",
4 | "version": "3.3.0",
5 | "description": "A place for your happy widget in every DOM.",
6 | "main": "dist/preact-habitat.js",
7 | "module": "dist/preact-habitat.es.js",
8 | "main:umd": "dist/preact-habitat.umd.js",
9 | "jsnext:main": "dist/preact-habitat.es.js",
10 | "homepage": "https://github.com/zouhir/preact-habitat/",
11 | "types": "src/habitat.d.ts",
12 | "author": {
13 | "name": "Zouhir Chahoud",
14 | "email": "zouhir@zouhir.org",
15 | "url": "http://zouhir.org/"
16 | },
17 | "scripts": {
18 | "clean": "rimraf dist",
19 | "build": "npm-run-all clean transpile minify",
20 | "transpile": "rollup -c",
21 | "minify": "uglifyjs dist/preact-habitat.js -cm toplevel -o dist/preact-habitat.js -p relative --in-source-map dist/preact-habitat.js.map --source-map dist/preact-habitat.js.map && uglifyjs dist/preact-habitat.umd.js -cm -o dist/preact-habitat.umd.js -p relative --in-source-map dist/preact-habitat.umd.js.map --source-map dist/preact-habitat.umd.js.map",
22 | "test": "jest",
23 | "test:watch": "jest --watchAll",
24 | "coverage": "jest --coverage",
25 | "prepublish": "npm run build"
26 | },
27 | "keywords": [
28 | "JavaScript",
29 | "preact",
30 | "react",
31 | "DOM",
32 | "preact in DOM",
33 | "virtual dom",
34 | "widget"
35 | ],
36 | "license": "MIT",
37 | "devDependencies": {
38 | "babel": "^6.23.0",
39 | "babel-eslint": "^7.2.3",
40 | "babel-jest": "^20.0.3",
41 | "babel-loader": "^7.0.0",
42 | "babel-plugin-transform-class-properties": "^6.24.1",
43 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
44 | "babel-plugin-transform-react-jsx": "^6.24.1",
45 | "babel-polyfill": "^6.23.0",
46 | "babel-preset-es2015": "^6.24.1",
47 | "babel-preset-es2015-minimal": "^2.1.0",
48 | "babel-preset-es2015-minimal-rollup": "^2.1.1",
49 | "babel-preset-stage-0": "^6.24.1",
50 | "babel-register": "^6.24.1",
51 | "chai": "^4.0.1",
52 | "cross-env": "^5.0.0",
53 | "eslint": "^3.19.0",
54 | "eslint-config-standard": "^10.2.1",
55 | "eslint-config-standard-preact": "^1.1.1",
56 | "eslint-plugin-promise": "^3.5.0",
57 | "eslint-plugin-react": "^7.0.1",
58 | "eslint-plugin-standard": "^3.0.1",
59 | "istanbul": "^0.4.5",
60 | "jest-cli": "^20.0.4",
61 | "jsdom": "^11.0.0",
62 | "mocha": "^3.4.2",
63 | "npm-run-all": "^4.0.2",
64 | "preact": "^10.0.0-rc.1",
65 | "regenerator-runtime": "^0.10.5",
66 | "replace-bundle-webpack-plugin": "^1.0.0",
67 | "rimraf": "^2.6.1",
68 | "rollup": "^0.41.6",
69 | "rollup-plugin-babel": "^2.7.1",
70 | "rollup-plugin-buble": "^0.15.0",
71 | "rollup-plugin-commonjs": "^8.1.0",
72 | "rollup-plugin-es3": "^1.0.3",
73 | "rollup-plugin-node-resolve": "^3.0.0",
74 | "simulant": "^0.2.2",
75 | "uglify-js": "^2.8.29"
76 | },
77 | "peerDependencies": {
78 | "preact": "*"
79 | },
80 | "dependencies": {},
81 | "jest": {
82 | "transform": {
83 | "^.+\\.js$": "babel-jest"
84 | },
85 | "moduleFileExtensions": [
86 | "js"
87 | ],
88 | "collectCoverageFrom": [
89 | "src/**/*.{js}"
90 | ]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import buble from 'rollup-plugin-buble';
2 | import fs from 'fs';
3 |
4 | const pkg = JSON.parse(fs.readFileSync('./package.json'));
5 |
6 | export default {
7 | entry: 'src/index.js',
8 | useStrict: false,
9 | sourceMap: true,
10 | external: ["preact"],
11 | plugins: [
12 | buble()
13 | ],
14 | targets: [
15 | { dest: pkg.main, format: 'cjs' },
16 | { dest: pkg.module, format: 'es' },
17 | { dest: pkg['main:umd'], format: 'umd', moduleName: pkg.amdName }
18 | ]
19 | };
--------------------------------------------------------------------------------
/src/habitat.d.ts:
--------------------------------------------------------------------------------
1 | declare module "preact-habitat" {
2 | import { ComponentFactory } from "preact";
3 | interface IHabitatRenderConfig {
4 | /**
5 | * DOM Element selector used to retrieve the DOM elements you want to mount
6 | * the widget in.
7 | */
8 | selector: string;
9 | /**
10 | * Default props to be rendered throughout widgets, you can replace each
11 | * value declaring props.
12 | * @default {}
13 | */
14 | defaultProps?: any;
15 | /**
16 | * Set to true if you want to use the parent DOM node as a host for your
17 | * widget without specifing any selectors.
18 | * @default true
19 | */
20 | inline?: boolean;
21 | /**
22 | * clean will remove all the innerHTML from the HTMl element the widget will
23 | * mount in.
24 | * @default false
25 | */
26 | clean?: boolean;
27 | /**
28 | * Whether the client will specify the selector. Overwrites selector option.
29 | * @default false
30 | */
31 | clientSpecified?: boolean;
32 | }
33 | interface IHabitat {
34 | /**
35 | * Renders the preact component into the DOM.
36 | * @param config Configuration object
37 | */
38 | render(config: IHabitatRenderConfig): void;
39 | }
40 | /**
41 | * A 900 Bytes module for that will make plugging in Preact components and
42 | * widgets in any CMS or website as fun as lego!
43 | * @param widget {ComponentFactory} Component to plug
44 | */
45 | export default function habitat(widget: ComponentFactory
): IHabitat;
46 | }
47 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { widgetDOMHostElements, preactRender } from "./lib";
2 |
3 | const habitat = Widget => {
4 | // Widget represents the Preact component we need to mount
5 | let widget = Widget;
6 | // preact root render helper
7 | let root = null;
8 |
9 | let render = (
10 | {
11 | selector = null,
12 | inline = false,
13 | clean = false,
14 | clientSpecified = false,
15 | defaultProps = {}
16 | } = {}
17 | ) => {
18 | let elements = widgetDOMHostElements({
19 | selector,
20 | inline,
21 | clientSpecified
22 | });
23 | let loaded = () => {
24 | if (elements.length > 0) {
25 | let elements = widgetDOMHostElements({
26 | selector,
27 | inline,
28 | clientSpecified
29 | });
30 |
31 | return preactRender(widget, elements, root, clean, defaultProps);
32 | }
33 | };
34 | loaded();
35 | document.addEventListener("DOMContentLoaded", loaded);
36 | document.addEventListener("load", loaded);
37 | };
38 |
39 | return { render };
40 | };
41 |
42 | export default habitat;
43 |
--------------------------------------------------------------------------------
/src/lib.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "preact";
2 | /**
3 | * Removes `-` fron a string and capetalize the letter after
4 | * example: data-props-hello-world => dataPropsHelloWorld
5 | * Used for props passed from host DOM element
6 | * @param {String} str string
7 | * @return {String} Capetalized string
8 | */
9 | const camelcasize = str => {
10 | return str.replace(/-([a-z])/gi, (all, letter) => {
11 | return letter.toUpperCase();
12 | });
13 | };
14 |
15 | /**
16 | * [getExecutedScript internal widget to provide the currently executed script]
17 | * @param {document} document [Browser document object]
18 | * @return {HTMLElement} [script Element]
19 | */
20 | const getExecutedScript = () => {
21 | return (
22 | document.currentScript ||
23 | (() => {
24 | let scripts = document.getElementsByTagName("script");
25 | return scripts[scripts.length - 1];
26 | })()
27 | );
28 | };
29 |
30 | /**
31 | * Get the props from a host element's data attributes
32 | * @param {Element} tag The host element
33 | * @return {Object} props object to be passed to the component
34 | */
35 | const collectPropsFromElement = (element, defaultProps = {}) => {
36 | let attrs = element.attributes;
37 |
38 | let props = Object.assign({}, defaultProps);
39 |
40 | // collect from element
41 | Object.keys(attrs).forEach(key => {
42 | if (attrs.hasOwnProperty(key)) {
43 | let dataAttrName = attrs[key].name;
44 | if (!dataAttrName || typeof dataAttrName !== "string") {
45 | return false;
46 | }
47 | let propName = dataAttrName.split(/(data-props?-)/).pop() || '';
48 | propName = camelcasize(propName);
49 | if (dataAttrName !== propName) {
50 | let propValue = attrs[key].nodeValue;
51 | props[propName] = propValue;
52 | }
53 | }
54 | });
55 |
56 | // check for child script text/props or application/json
57 | [].forEach.call(element.getElementsByTagName('script'), scrp => {
58 | let propsObj = {}
59 | if(scrp.hasAttribute('type')) {
60 | if (
61 | scrp.getAttribute("type") !== "text/props" &&
62 | scrp.getAttribute("type") !== "application/json"
63 | )
64 | return;
65 | try {
66 | propsObj = JSON.parse(scrp.innerHTML);
67 | } catch(e) {
68 | throw new Error(e)
69 | }
70 | Object.assign(props, propsObj)
71 | }
72 | });
73 |
74 | return props;
75 | };
76 |
77 | const getHabitatSelectorFromClient = (currentScript) => {
78 | let scriptTagAttrs = currentScript.attributes;
79 | let selector = null;
80 | // check for another props attached to the tag
81 | Object.keys(scriptTagAttrs).forEach(key => {
82 | if (scriptTagAttrs.hasOwnProperty(key)) {
83 | const dataAttrName = scriptTagAttrs[key].name;
84 | if (dataAttrName === 'data-mount-in') {
85 | selector = scriptTagAttrs[key].nodeValue;
86 | }
87 | }
88 | });
89 | return selector
90 | }
91 |
92 | /**
93 | * Return array of 0 or more elements that will host our widget
94 | * @param {id} attrId the data widget id attribute the host should have
95 | * @param {document} scope Docuemnt object or DOM Element as a scope
96 | * @return {Array} Array of matching habitats
97 | */
98 | const widgetDOMHostElements = (
99 | { selector, inline, clientSpecified}
100 | ) => {
101 | let hostNodes = [];
102 | let currentScript = getExecutedScript();
103 |
104 | if (inline === true) {
105 | let parentNode = currentScript.parentNode;
106 | hostNodes.push(parentNode);
107 | }
108 | if (clientSpecified === true && !selector) {
109 | // user did not specify where to mount - get it from script tag attributes
110 | selector = getHabitatSelectorFromClient(currentScript);
111 | }
112 | if (selector) {
113 | [].forEach.call(document.querySelectorAll(selector), queriedTag => {
114 | hostNodes.push(queriedTag);
115 | });
116 | }
117 | return hostNodes;
118 | };
119 |
120 | /**
121 | * preact render function that will be queued if the DOM is not ready
122 | * and executed immeidatly if DOM is ready
123 | */
124 | const preactRender = (widget, hostElements, root, cleanRoot, defaultProps) => {
125 | hostElements.forEach(elm => {
126 | let hostNode = elm;
127 | if (hostNode._habitat) {
128 | return;
129 | }
130 | hostNode._habitat = true;
131 | let props = collectPropsFromElement(elm, defaultProps) || defaultProps;
132 | if(cleanRoot) {
133 | hostNode.innerHTML = "";
134 | }
135 | return render(h(widget, props), hostNode, root);
136 | });
137 | };
138 |
139 | export {
140 | collectPropsFromElement,
141 | widgetDOMHostElements,
142 | getExecutedScript,
143 | camelcasize,
144 | preactRender,
145 | getHabitatSelectorFromClient
146 | };
147 |
--------------------------------------------------------------------------------
/src/test/habitat.test.js:
--------------------------------------------------------------------------------
1 | import { h, Component } from "preact";
2 | import simulant from "simulant";
3 |
4 | import {
5 | collectPropsFromElement,
6 | widgetDOMHostElements,
7 | getExecutedScript,
8 | camelcasize
9 | } from "../lib";
10 |
11 | import habitat from "../index";
12 |
13 | const TEST_TITLE = "Hello, World!";
14 |
15 | class TitleComponent extends Component {
16 | render() {
17 | return (
18 |
19 | {TEST_TITLE}
20 |
21 | );
22 | }
23 | }
24 |
25 | describe("Module API Specs", () => {
26 | it("should export habitat factory function", () => {
27 | expect(typeof habitat).toBe("function");
28 | });
29 |
30 | it("should return render function form the habitat factory", () => {
31 | expect(typeof habitat().render).toBe("function");
32 | });
33 | });
34 |
35 | /**
36 | * Renders the widget based on client specified attributes
37 | */
38 | describe("Habitat Client Control Renderer", () => {
39 | it("should inline the widget and render it once", () => {
40 | document.body.innerHTML = `
41 |
42 | `;
43 | let hb = habitat(TitleComponent);
44 | hb.render({ inline: true });
45 |
46 | let widgets = document.querySelectorAll(".test");
47 | expect(document.body.innerHTML).toContain(TEST_TITLE);
48 | expect(widgets.length).toBe(1);
49 | });
50 | it("should render the widget in 3 habitat elements", () => {
51 | document.body.innerHTML = `
52 |
53 |
54 |
55 | `;
56 | let hb = habitat(TitleComponent);
57 | hb.render({ selector: '[data-widget="my-widget"]' });
58 |
59 | let widgets = document.querySelectorAll(".test");
60 | expect(document.body.innerHTML).toContain(TEST_TITLE);
61 | expect(widgets.length).toBe(3);
62 | });
63 | it("should render 2 custom attributes habitat elements", () => {
64 | document.body.innerHTML = `
65 |
66 |
67 | `
68 | let hb = habitat(TitleComponent);
69 | hb.render({selector: '[data-widget-tv="tv-player"]'});
70 |
71 | let widgets = document.querySelectorAll(".test");
72 | expect(document.body.innerHTML).toContain(TEST_TITLE);
73 | expect(widgets.length).toBe(2);
74 | });
75 |
76 | it("should render 1 widget and not clean its content", () => {
77 | document.body.innerHTML = `
78 | LOADING BIG TABLE
79 | `
80 | let hb = habitat(TitleComponent);
81 | hb.render({ selector: '[data-table-widget="datatable"]' });
82 |
83 | let widgets = document.querySelectorAll('[data-table-widget="datatable"]');
84 | expect(document.body.innerHTML).toContain("LOADING BIG TABLE");
85 | expect(widgets[0].innerHTML).toContain(TEST_TITLE);
86 | expect(widgets.length).toBe(1);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/src/test/lib.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import {
3 | collectPropsFromElement,
4 | widgetDOMHostElements,
5 | getExecutedScript,
6 | camelcasize,
7 | getHabitatSelectorFromClient
8 | } from '../lib';
9 |
10 | import habitat from '../index';
11 |
12 | describe('Helper utility: Camel Casing for props', () => {
13 | it('should not camcelCase names with no dashes', () => {
14 | const propOne = 'somepropname';
15 | // document must find current script tag
16 | expect(camelcasize(propOne)).toBe(propOne);
17 | });
18 |
19 | it('should camcelCase prop names with dashes `-`', () => {
20 | const propOne = 'some-prop-name';
21 | // document must find current script tag
22 | expect(camelcasize(propOne)).toBe('somePropName');
23 | });
24 | })
25 |
26 | describe('Helper utility: Client DOM querying with widgetDOMHostElements', () => {
27 | it('should find host using data attribute', () => {
28 | document.body.innerHTML = `
29 |
30 |
31 |
32 | `
33 | const hostHabitats = widgetDOMHostElements({ selector: '[data-widget="my-widget"]', clientSpecified: false, inline: false, clean: false });
34 | // document must find current script tag
35 | expect(hostHabitats.length).toBe(3);
36 | });
37 |
38 | it('should find host using class name', () => {
39 | document.body.innerHTML = `
40 |
41 |
42 |
43 | `
44 | const hostHabitats = widgetDOMHostElements({selector: '.classy-widget', clientSpecified: false, inline: false, clean: false });
45 | // document must find current script tag
46 | expect(hostHabitats.length).toBe(3);
47 | });
48 |
49 | it('should find host using ID', () => {
50 | document.body.innerHTML = `
51 |
52 | `
53 | const hostHabitats = widgetDOMHostElements({ selector: '#idee-widget', clientSpecified: false, inline: false, clean: false });
54 | // document must find current script tag
55 | expect(hostHabitats.length).toBe(1);
56 | });
57 |
58 | it('should get the currently getting executed script tag', () => {
59 | document.body.innerHTML = `
60 |
61 | `
62 | expect(getExecutedScript(document)).toBeDefined();
63 | });
64 |
65 | it('should get habitats selectors from client script itself', () => {
66 | document.body.innerHTML = `
67 |
68 | `
69 | let currentScript = document.getElementById('find-mount-here')
70 | expect(getHabitatSelectorFromClient(currentScript)).toBe('.my-widget');
71 | });
72 | });
73 |
74 |
75 | describe('Helper utility: collecting Client DOM props with collectPropsFromElement', () => {
76 | it('should pass props down from the client\'s div', () => {
77 | document.body.innerHTML = `
78 |
79 | `
80 | const habitatDiv = document.getElementById('sucess-props-check');
81 | const expectedProps = {
82 | name: 'zouhir',
83 | key: '11001100'
84 | };
85 | const propsObj = collectPropsFromElement(habitatDiv);
86 | // document must find current script tag
87 | expect(propsObj).toEqual(expectedProps);
88 | });
89 |
90 | it('should accept data-props- as well as data-prop attributes on the div', () => {
91 | document.body.innerHTML = `
92 |
93 | `
94 | const habitatDiv = document.getElementById('sucess-props-check');
95 | const expectedProps = {
96 | name: 'zouhir',
97 | key: '11001100'
98 | };
99 | const propsObj = collectPropsFromElement(habitatDiv);
100 | // document must find current script tag
101 | expect(propsObj).toEqual(expectedProps);
102 | });
103 |
104 | it('should collect props from prop script', () => {
105 | document.body.innerHTML = `
106 |
107 |
112 |
113 |
116 |
117 | `
118 | const habitatDiv = document.getElementById('parent');
119 |
120 | const propsObj = collectPropsFromElement(habitatDiv);
121 |
122 | const expectedProps = {
123 | "name": "zouhir"
124 | }
125 | // document must find current script tag
126 | expect(propsObj).toEqual(expectedProps);
127 | });
128 |
129 |
130 | it('should collect props from application/json script', () => {
131 | document.body.innerHTML = `
132 |
133 |
138 |
139 |
142 |
143 | `
144 | const habitatDiv = document.getElementById('parent');
145 |
146 | const propsObj = collectPropsFromElement(habitatDiv);
147 |
148 | const expectedProps = {
149 | "name": "zouhir"
150 | }
151 | // document must find current script tag
152 | expect(propsObj).toEqual(expectedProps);
153 | });
154 |
155 | it('should collect props from prop script and merge with default props', () => {
156 | document.body.innerHTML = `
157 |
158 |
163 |
164 |
167 |
168 | `
169 | const habitatDiv = document.getElementById('parent');
170 |
171 | const propsObj = collectPropsFromElement(habitatDiv, { project: "habitat" });
172 |
173 | const expectedProps = {
174 | "name": "zouhir",
175 | "project": "habitat"
176 | };
177 | // document must find current script tag
178 | expect(propsObj).toEqual(expectedProps);
179 | });
180 |
181 | it('should collect props from prop script replace default props', () => {
182 | document.body.innerHTML = `
183 |
184 |
190 |
191 |
194 |
195 | `
196 | const habitatDiv = document.getElementById('parent');
197 |
198 | const propsObj = collectPropsFromElement(habitatDiv, { project: "habitat" });
199 |
200 | const expectedProps = {
201 | "name": "zouhir",
202 | "project": "foobar"
203 | };
204 | // document must find current script tag
205 | expect(propsObj).toEqual(expectedProps);
206 | });
207 |
208 |
209 | it('should collect props from prop multiple scripts', () => {
210 | document.body.innerHTML = `
211 |
212 |
217 |
218 |
223 |
224 |
227 |
228 | `
229 | const habitatDiv = document.getElementById('parent');
230 |
231 | const propsObj = collectPropsFromElement(habitatDiv);
232 |
233 | const expectedProps = {
234 | "libName": "preact-habitat",
235 | "dev": "zouhir",
236 | "lib": "preact"
237 | }
238 | // document must find current script tag
239 | expect(propsObj).toEqual(expectedProps);
240 | });
241 | })
--------------------------------------------------------------------------------