├── .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 |
2 | 3 |
4 | 5 | [![Build Status][build-badge]][build] 6 | [![Code Coverage][coverage-badge]][coverage] 7 | [![downloads][downloads-badge]][npmcharts] 8 | [![version][version-badge]][package] 9 | [![gzip size][gzip-badge]][unpkg-dist] 10 | [![module formats: umd, cjs, and es][module-formats-badge]][unpkg-dist] 11 | [![Supports Preact and React][preact-badge]][preact] 12 | [![MIT License][license-badge]][LICENSE] 13 | 14 | 15 | ## Preact Habitat 16 | 17 | A 900 Bytes module for that will make plugging in Preact components and widgets in any CMS or website as fun as lego! 18 | 19 | ### Demos 20 | 21 | Login Widget [Source Code 💻](https://github.com/zouhir/preact-habitat/tree/master/examples/login-form) 22 | 23 | Login Widget Integration pen [Codepen Demo 🖋](https://codepen.io/zouhir/pen/brrOPB?editors=1000) 24 | 25 | ### Installation 26 | 27 | ```bash 28 | npm install --save preact-habitat 29 | ``` 30 | 31 | ### Core Features 32 | 33 | - 2 ways to passing props from DOM. 34 | - Multiple rendering options. 35 | - Light weight ( < 1KB ). 36 | - Compatible with React widgets through preact-compat. 37 | - In use in high traffic web applications. 38 | 39 | ### Basic Usage Example 40 | 41 | ```js 42 | import habitat from 'preact-habitat'; 43 | import WidgetAwesome from './components/WidgetAwesome'; 44 | 45 | const { render } = habitat(WidgetAwesome); 46 | 47 | /** 48 | ** other selecors options: 49 | ** 50 | ** ".classname" for querying DOM element by its class name 51 | ** 52 | ** "#div-id" for querying DOM element by its ID value 53 | ** 54 | ** "[data-attribute-example='widget-here']" for querying DOM element by its data attribute name & val 55 | ** 56 | **/ 57 | 58 | render({ 59 | selector: '.some-class', // Searches and mounts in
60 | defaultProps: undefined, // Default props for all widgets 61 | inline: false, 62 | clean: false, 63 | clientSpecified: false 64 | }); 65 | ``` 66 | 67 | in `webpack.config.js` or any other build tool bundle output format should be `UMD`: 68 | 69 | ```js 70 | output: { 71 | libraryTarget: 'umd' 72 | } 73 | ``` 74 | 75 | in the DOM you'd like to mount your widget in: 76 | 77 | ```html 78 |
79 | 86 |
87 | ``` 88 | 89 | Now, build your production ready preact widget and you're all set, TADA! 🎉 90 | 91 | ## API Docs 92 | 93 | ### habitat(...) 94 | 95 | accepts a single Preact component as its only argument 96 | 97 | ##### example: 98 | ```js 99 | import { h } form 'preact'; 100 | import habitat from 'preact-habitat'; 101 | 102 | const Widget = () =>

Hello, World!

; 103 | 104 | const { render } = habitat(Widget); // NOTE: pass Widget and not 105 | 106 | render({ 107 | ... 108 | }); 109 | ``` 110 | 111 | ### render(options) 112 | 113 | render function accepts an options Object which supports the following properties: 114 | 115 | #### option.selector 116 | 117 | >String: `.myclass`, `#myid`, `[data-selector="my-data-attr"]` 118 | 119 | DOM Element selector used to retrieve the DOM elements you want to mount the widget in 120 | 121 | #### option.defaultProps 122 | 123 | > Object: {} || undefined (default) 124 | 125 | Default props to be rendered throughout widgets, you can replace each value [declaring props](#passing-props). 126 | 127 | #### option.inline 128 | > Boolean: true || false (default) 129 | 130 | Set to true if you want to use the parent DOM node as a host for your widget without specifing any selectors. 131 | 132 | example: 133 | 134 | ```html 135 |
136 | 138 | 139 |
140 | ``` 141 | 142 | #### option.clean 143 | > Boolean: true || false (default) 144 | 145 | clean will remove all the innerHTML from the HTMl element the widget will mount in. 146 | 147 | example: 148 | 149 | if we set the widget to be mounted inside the selector ".beautiful-container" with {clean: true} it will remove the Loading div as soon as it renders. 150 | 151 | ```html 152 |
153 |
LOADING...
154 |
155 | 156 | 157 | ``` 158 | 159 | #### option.clientSpecified 160 | > Boolean: true || false (default) 161 | 162 | This option allows who ever using the script to specifit the selector which they'd like to mount the widget in 163 | 164 | ```html 165 |
166 |
LOADING...
167 |
168 | 169 | 170 | ``` 171 | 172 | ### Passing Props 173 | 174 | There are 2 ways to pass props, either via data-attributes or application/json script tag 175 | 176 | #### via props script 177 | 178 | Simply add a ` 189 | 190 | ``` 191 | 192 | #### via data-attribute 193 | 194 | the data attribute has to always start with `data-prop-` examples: 195 | 196 | `data-prop-name` will be available in your component as `name` 197 | 198 | `data-prop-version` will be available in your component as `version` 199 | 200 | `data-prop-theme-color` will be available in your component as `themeColor` *NOTE* the lowerCamelCase when there's a - 201 | 202 | ```html 203 |
204 | 205 |
206 | ``` 207 | 208 | ## License 209 | [MIT](LICENSE) - Copyright (c) [Zouhir Chahoud](https://zouhir.org) 210 | 211 | ## Credits 212 | Artwork By: [Oleg Turbaba, Dribble](https://dribbble.com/turbaba) 213 | 214 | 215 | [build-badge]: https://img.shields.io/travis/zouhir/preact-habitat.svg?style=flat-square 216 | [build]: https://travis-ci.org/zouhir/preact-habitat 217 | [coverage-badge]: https://img.shields.io/codecov/c/github/zouhir/preact-habitat.svg?style=flat-square 218 | [coverage]: https://codecov.io/github/zouhir/preact-habitat 219 | [version-badge]: https://img.shields.io/npm/v/preact-habitat.svg?style=flat-square 220 | [package]: https://www.npmjs.com/package/preact-habitat 221 | [downloads-badge]: https://img.shields.io/npm/dm/preact-habitat.svg?style=flat-square 222 | [npmcharts]: http://npmcharts.com/compare/preact-habitat 223 | [license-badge]: https://img.shields.io/npm/l/preact-habitat.svg?style=flat-square 224 | [license]: https://github.com/zouhir/preact-habitat/blob/master/LICENSE 225 | [preact-badge]: https://img.shields.io/badge/%E2%9A%9B%EF%B8%8F-preact-6F2FBF.svg?style=flat-square 226 | [preact]: https://preactjs.com 227 | [gzip-badge]: http://img.badgesize.io/https://unpkg.com/preact-habitat@3.0.2/dist/preact-habitat.umd.js?compression=gzip&label=gzip%20size&style=flat-square 228 | [unpkg-dist]: https://unpkg.com/preact-habitat/dist/ 229 | [module-formats-badge]: https://img.shields.io/badge/module%20formats-umd%2C%20cjs%2C%20es-green.svg?style=flat-square 230 | [github-star-badge]: https://img.shields.io/github/stars/zouhir/preact-habitat.svg?style=social 231 | [github-star]: https://github.com/zouhir/preact-habitat/stargazers 232 | -------------------------------------------------------------------------------- /dist/preact-habitat.es.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | 3 | /** 4 | * Removes `-` fron a string and capetalize the letter after 5 | * example: data-props-hello-world => dataPropsHelloWorld 6 | * Used for props passed from host DOM element 7 | * @param {String} str string 8 | * @return {String} Capetalized string 9 | */ 10 | var camelcasize = function (str) { 11 | return str.replace(/-([a-z])/gi, function (all, letter) { 12 | return letter.toUpperCase(); 13 | }); 14 | }; 15 | 16 | /** 17 | * [getExecutedScript internal widget to provide the currently executed script] 18 | * @param {document} document [Browser document object] 19 | * @return {HTMLElement} [script Element] 20 | */ 21 | var getExecutedScript = function () { 22 | return ( 23 | document.currentScript || 24 | (function () { 25 | var scripts = document.getElementsByTagName("script"); 26 | return scripts[scripts.length - 1]; 27 | })() 28 | ); 29 | }; 30 | 31 | /** 32 | * Get the props from a host element's data attributes 33 | * @param {Element} tag The host element 34 | * @return {Object} props object to be passed to the component 35 | */ 36 | var collectPropsFromElement = function (element, defaultProps) { 37 | if ( defaultProps === void 0 ) defaultProps = {}; 38 | 39 | var attrs = element.attributes; 40 | 41 | var props = Object.assign({}, defaultProps); 42 | 43 | // collect from element 44 | Object.keys(attrs).forEach(function (key) { 45 | if (attrs.hasOwnProperty(key)) { 46 | var dataAttrName = attrs[key].name; 47 | if (!dataAttrName || typeof dataAttrName !== "string") { 48 | return false; 49 | } 50 | var propName = dataAttrName.split(/(data-props?-)/).pop() || ''; 51 | propName = camelcasize(propName); 52 | if (dataAttrName !== propName) { 53 | var propValue = attrs[key].nodeValue; 54 | props[propName] = propValue; 55 | } 56 | } 57 | }); 58 | 59 | // check for child script text/props or application/json 60 | [].forEach.call(element.getElementsByTagName('script'), function (scrp) { 61 | var propsObj = {}; 62 | if(scrp.hasAttribute('type')) { 63 | if ( 64 | scrp.getAttribute("type") !== "text/props" && 65 | scrp.getAttribute("type") !== "application/json" 66 | ) 67 | { return; } 68 | try { 69 | propsObj = JSON.parse(scrp.innerHTML); 70 | } catch(e) { 71 | throw new Error(e) 72 | } 73 | Object.assign(props, propsObj); 74 | } 75 | }); 76 | 77 | return props; 78 | }; 79 | 80 | var getHabitatSelectorFromClient = function (currentScript) { 81 | var scriptTagAttrs = currentScript.attributes; 82 | var selector = null; 83 | // check for another props attached to the tag 84 | Object.keys(scriptTagAttrs).forEach(function (key) { 85 | if (scriptTagAttrs.hasOwnProperty(key)) { 86 | var dataAttrName = scriptTagAttrs[key].name; 87 | if (dataAttrName === 'data-mount-in') { 88 | selector = scriptTagAttrs[key].nodeValue; 89 | } 90 | } 91 | }); 92 | return selector 93 | }; 94 | 95 | /** 96 | * Return array of 0 or more elements that will host our widget 97 | * @param {id} attrId the data widget id attribute the host should have 98 | * @param {document} scope Docuemnt object or DOM Element as a scope 99 | * @return {Array} Array of matching habitats 100 | */ 101 | var widgetDOMHostElements = function ( 102 | ref 103 | ) { 104 | var selector = ref.selector; 105 | var inline = ref.inline; 106 | var clientSpecified = ref.clientSpecified; 107 | 108 | var hostNodes = []; 109 | var currentScript = getExecutedScript(); 110 | 111 | if (inline === true) { 112 | var parentNode = currentScript.parentNode; 113 | hostNodes.push(parentNode); 114 | } 115 | if (clientSpecified === true && !selector) { 116 | // user did not specify where to mount - get it from script tag attributes 117 | selector = getHabitatSelectorFromClient(currentScript); 118 | } 119 | if (selector) { 120 | [].forEach.call(document.querySelectorAll(selector), function (queriedTag) { 121 | hostNodes.push(queriedTag); 122 | }); 123 | } 124 | return hostNodes; 125 | }; 126 | 127 | /** 128 | * preact render function that will be queued if the DOM is not ready 129 | * and executed immeidatly if DOM is ready 130 | */ 131 | var preactRender = function (widget, hostElements, root, cleanRoot, defaultProps) { 132 | hostElements.forEach(function (elm) { 133 | var hostNode = elm; 134 | if (hostNode._habitat) { 135 | return; 136 | } 137 | hostNode._habitat = true; 138 | var props = collectPropsFromElement(elm, defaultProps) || defaultProps; 139 | if(cleanRoot) { 140 | hostNode.innerHTML = ""; 141 | } 142 | return render(h(widget, props), hostNode, root); 143 | }); 144 | }; 145 | 146 | var habitat = function (Widget) { 147 | // Widget represents the Preact component we need to mount 148 | var widget = Widget; 149 | // preact root render helper 150 | var root = null; 151 | 152 | var render$$1 = function ( 153 | ref 154 | ) { 155 | if ( ref === void 0 ) ref = {}; 156 | var selector = ref.selector; if ( selector === void 0 ) selector = null; 157 | var inline = ref.inline; if ( inline === void 0 ) inline = false; 158 | var clean = ref.clean; if ( clean === void 0 ) clean = false; 159 | var clientSpecified = ref.clientSpecified; if ( clientSpecified === void 0 ) clientSpecified = false; 160 | var defaultProps = ref.defaultProps; if ( defaultProps === void 0 ) defaultProps = {}; 161 | 162 | var elements = widgetDOMHostElements({ 163 | selector: selector, 164 | inline: inline, 165 | clientSpecified: clientSpecified 166 | }); 167 | var loaded = function () { 168 | if (elements.length > 0) { 169 | var elements$1 = widgetDOMHostElements({ 170 | selector: selector, 171 | inline: inline, 172 | clientSpecified: clientSpecified 173 | }); 174 | 175 | return preactRender(widget, elements$1, root, clean, defaultProps); 176 | } 177 | }; 178 | loaded(); 179 | document.addEventListener("DOMContentLoaded", loaded); 180 | document.addEventListener("load", loaded); 181 | }; 182 | 183 | return { render: render$$1 }; 184 | }; 185 | 186 | export default habitat; 187 | //# sourceMappingURL=preact-habitat.es.js.map 188 | -------------------------------------------------------------------------------- /dist/preact-habitat.es.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"preact-habitat.es.js","sources":["../src/lib.js","../src/index.js"],"sourcesContent":["import { h, render } from \"preact\";\n/**\n * Removes `-` fron a string and capetalize the letter after\n * example: data-props-hello-world => dataPropsHelloWorld\n * Used for props passed from host DOM element\n * @param {String} str string\n * @return {String} Capetalized string\n */\nconst camelcasize = str => {\n return str.replace(/-([a-z])/gi, (all, letter) => {\n return letter.toUpperCase();\n });\n};\n\n/**\n * [getExecutedScript internal widget to provide the currently executed script]\n * @param {document} document [Browser document object]\n * @return {HTMLElement} [script Element]\n */\nconst getExecutedScript = () => {\n return (\n document.currentScript ||\n (() => {\n let scripts = document.getElementsByTagName(\"script\");\n return scripts[scripts.length - 1];\n })()\n );\n};\n\n/**\n * Get the props from a host element's data attributes\n * @param {Element} tag The host element\n * @return {Object} props object to be passed to the component\n */\nconst collectPropsFromElement = (element, defaultProps = {}) => {\n let attrs = element.attributes;\n\n let props = Object.assign({}, defaultProps);\n\n // collect from element\n Object.keys(attrs).forEach(key => {\n if (attrs.hasOwnProperty(key)) {\n let dataAttrName = attrs[key].name;\n if (!dataAttrName || typeof dataAttrName !== \"string\") {\n return false;\n }\n let propName = dataAttrName.split(/(data-props?-)/).pop() || '';\n propName = camelcasize(propName);\n if (dataAttrName !== propName) {\n let propValue = attrs[key].nodeValue;\n props[propName] = propValue;\n }\n }\n });\n\n // check for child script text/props or application/json\n [].forEach.call(element.getElementsByTagName('script'), scrp => {\n let propsObj = {}\n if(scrp.hasAttribute('type')) {\n if (\n scrp.getAttribute(\"type\") !== \"text/props\" &&\n scrp.getAttribute(\"type\") !== \"application/json\"\n )\n return;\n try {\n propsObj = JSON.parse(scrp.innerHTML);\n } catch(e) {\n throw new Error(e)\n }\n Object.assign(props, propsObj)\n }\n }); \n\n return props;\n};\n\nconst getHabitatSelectorFromClient = (currentScript) => {\n let scriptTagAttrs = currentScript.attributes;\n let selector = null;\n // check for another props attached to the tag\n Object.keys(scriptTagAttrs).forEach(key => {\n if (scriptTagAttrs.hasOwnProperty(key)) {\n const dataAttrName = scriptTagAttrs[key].name;\n if (dataAttrName === 'data-mount-in') {\n selector = scriptTagAttrs[key].nodeValue;\n }\n }\n });\n return selector\n}\n\n/**\n * Return array of 0 or more elements that will host our widget\n * @param {id} attrId the data widget id attribute the host should have\n * @param {document} scope Docuemnt object or DOM Element as a scope\n * @return {Array} Array of matching habitats\n */\nconst widgetDOMHostElements = (\n { selector, inline, clientSpecified}\n) => {\n let hostNodes = [];\n let currentScript = getExecutedScript();\n\n if (inline === true) {\n let parentNode = currentScript.parentNode;\n hostNodes.push(parentNode);\n }\n if (clientSpecified === true && !selector) {\n // user did not specify where to mount - get it from script tag attributes\n selector = getHabitatSelectorFromClient(currentScript);\n }\n if (selector) {\n [].forEach.call(document.querySelectorAll(selector), queriedTag => {\n hostNodes.push(queriedTag);\n });\n }\n return hostNodes;\n};\n\n/**\n * preact render function that will be queued if the DOM is not ready\n * and executed immeidatly if DOM is ready\n */\nconst preactRender = (widget, hostElements, root, cleanRoot, defaultProps) => {\n hostElements.forEach(elm => {\n let hostNode = elm;\n if (hostNode._habitat) {\n return; \n }\n hostNode._habitat = true;\n let props = collectPropsFromElement(elm, defaultProps) || defaultProps;\n if(cleanRoot) {\n hostNode.innerHTML = \"\";\n }\n return render(h(widget, props), hostNode, root);\n });\n};\n\nexport {\n collectPropsFromElement,\n widgetDOMHostElements,\n getExecutedScript,\n camelcasize,\n preactRender,\n getHabitatSelectorFromClient\n};\n","import { widgetDOMHostElements, preactRender } from \"./lib\";\n\nconst habitat = Widget => {\n // Widget represents the Preact component we need to mount\n let widget = Widget;\n // preact root render helper\n let root = null;\n\n let render = (\n {\n selector = null,\n inline = false,\n clean = false,\n clientSpecified = false,\n defaultProps = {}\n } = {}\n ) => {\n let elements = widgetDOMHostElements({\n selector,\n inline,\n clientSpecified\n });\n let loaded = () => {\n if (elements.length > 0) {\n let elements = widgetDOMHostElements({\n selector,\n inline,\n clientSpecified\n });\n\n return preactRender(widget, elements, root, clean, defaultProps);\n }\n };\n loaded();\n document.addEventListener(\"DOMContentLoaded\", loaded);\n document.addEventListener(\"load\", loaded);\n };\n\n return { render };\n};\n\nexport default habitat;\n"],"names":["const","let","render","elements"],"mappings":"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"} -------------------------------------------------------------------------------- /dist/preact-habitat.js: -------------------------------------------------------------------------------- 1 | var e=require("preact"),t=function(e){return e.replace(/-([a-z])/gi,function(e,t){return t.toUpperCase()})},n=function(){return document.currentScript||function(){var e=document.getElementsByTagName("script");return e[e.length-1]}()},r=function(e,n){void 0===n&&(n={});var r=e.attributes,i=Object.assign({},n);return Object.keys(r).forEach(function(e){if(r.hasOwnProperty(e)){var n=r[e].name;if(!n||"string"!=typeof n)return!1;var a=n.split(/(data-props?-)/).pop()||"";if(a=t(a),n!==a){var o=r[e].nodeValue;i[a]=o}}}),[].forEach.call(e.getElementsByTagName("script"),function(e){var t={};if(e.hasAttribute("type")){if("text/props"!==e.getAttribute("type")&&"application/json"!==e.getAttribute("type"))return;try{t=JSON.parse(e.innerHTML)}catch(e){throw new Error(e)}Object.assign(i,t)}}),i},i=function(e){var t=e.attributes,n=null;return Object.keys(t).forEach(function(e){if(t.hasOwnProperty(e)){"data-mount-in"===t[e].name&&(n=t[e].nodeValue)}}),n},a=function(e){var t=e.selector,r=e.inline,a=e.clientSpecified,o=[],c=n();if(!0===r){var u=c.parentNode;o.push(u)}return!0!==a||t||(t=i(c)),t&&[].forEach.call(document.querySelectorAll(t),function(e){o.push(e)}),o},o=function(t,n,i,a,o){n.forEach(function(n){var c=n;if(!c._habitat){c._habitat=!0;var u=r(n,o)||o;return a&&(c.innerHTML=""),e.render(e.h(t,u),c,i)}})},c=function(e){var t=e;return{render:function(e){void 0===e&&(e={});var n=e.selector;void 0===n&&(n=null);var r=e.inline;void 0===r&&(r=!1);var i=e.clean;void 0===i&&(i=!1);var c=e.clientSpecified;void 0===c&&(c=!1);var u=e.defaultProps;void 0===u&&(u={});var l=a({selector:n,inline:r,clientSpecified:c}),f=function(){if(l.length>0){var e=a({selector:n,inline:r,clientSpecified:c});return o(t,e,null,i,u)}};f(),document.addEventListener("DOMContentLoaded",f),document.addEventListener("load",f)}}};module.exports=c; 2 | //# sourceMappingURL=preact-habitat.js.map -------------------------------------------------------------------------------- /dist/preact-habitat.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/lib.js","../src/index.js"],"names":["camelcasize","str","replace","all","letter","toUpperCase","getExecutedScript","document","currentScript","let","scripts","getElementsByTagName","length","collectPropsFromElement","element","defaultProps","attrs","attributes","props","Object","assign","keys","forEach","key","hasOwnProperty","dataAttrName","name","propName","split","pop","propValue","nodeValue","call","scrp","propsObj","hasAttribute","getAttribute","JSON","parse","innerHTML","e","Error","getHabitatSelectorFromClient","scriptTagAttrs","selector","widgetDOMHostElements","ref","inline","clientSpecified","hostNodes","parentNode","push","querySelectorAll","queriedTag","preactRender","widget","hostElements","root","cleanRoot","elm","hostNode","_habitat","render","h","habitat","Widget","elements","loaded","clean","addEventListener"],"mappings":"wBAQMA,EAAc,SAAAC,GAClB,MAAOA,GAAIC,QAAQ,aAAc,SAACC,EAAKC,GACrC,MAAOA,GAAOC,iBASZC,EAAoB,WACxB,MACEC,UAASC,eACT,WACEC,GAAIC,GAAUH,SAASI,qBAAqB,SAC5C,OAAOD,GAAQA,EAAQE,OAAS,OAUhCC,EAA0B,SAACC,EAASC,qBACxCN,IAAIO,GAAQF,EAAQG,WAEhBC,EAAQC,OAAOC,UAAWL,EAoC9B,OAjCAI,QAAOE,KAAKL,GAAOM,QAAQ,SAAAC,GACzB,GAAIP,EAAMQ,eAAeD,GAAM,CAC7Bd,GAAIgB,GAAeT,EAAMO,GAAKG,IAC9B,KAAKD,GAAwC,gBAAjBA,GAC1B,OAAO,CAEThB,IAAIkB,GAAWF,EAAaG,MAAM,kBAAkBC,OAAS,EAE7D,IADAF,EAAW3B,EAAY2B,GACnBF,IAAiBE,EAAU,CAC7BlB,GAAIqB,GAAYd,EAAMO,GAAKQ,SAC3Bb,GAAMS,GAAYG,SAMrBR,QAAQU,KAAKlB,EAAQH,qBAAqB,UAAW,SAAAsB,GACtDxB,GAAIyB,KACJ,IAAGD,EAAKE,aAAa,QAAS,CAC5B,GACgC,eAA9BF,EAAKG,aAAa,SACY,qBAA9BH,EAAKG,aAAa,QAElB,MACF,KACEF,EAAWG,KAAKC,MAAML,EAAKM,WAC3B,MAAMC,GACN,KAAM,IAAIC,OAAMD,GAElBrB,OAAOC,OAAOF,EAAOgB,MAIlBhB,GAGHwB,EAA+B,SAAClC,GACpCC,GAAIkC,GAAiBnC,EAAcS,WAC/B2B,EAAW,IAUf,OARAzB,QAAOE,KAAKsB,GAAgBrB,QAAQ,SAAAC,GAClC,GAAIoB,EAAenB,eAAeD,GAAM,CAEjB,kBADAoB,EAAepB,GAAKG,OAEvCkB,EAAWD,EAAepB,GAAKQ,cAI9Ba,GASHC,EAAwB,SAC5BC,MAAEF,GAAQE,EAAAF,SAAEG,EAAMD,EAAAC,OAAEC,EAAeF,EAAAE,gBAE/BC,KACAzC,EAAgBF,GAEpB,KAAe,IAAXyC,EAAiB,CACnBtC,GAAIyC,GAAa1C,EAAc0C,UAC/BD,GAAUE,KAAKD,GAWjB,OATwB,IAApBF,GAA6BJ,IAE/BA,EAAWF,EAA6BlC,IAEtCoC,MACCtB,QAAQU,KAAKzB,SAAS6C,iBAAiBR,GAAW,SAAAS,GACnDJ,EAAUE,KAAKE,KAGZJ,GAOHK,EAAe,SAACC,EAAQC,EAAcC,EAAMC,EAAW3C,GAC3DyC,EAAalC,QAAQ,SAAAqC,GACnBlD,GAAImD,GAAWD,CACf,KAAIC,EAASC,SAAb,CAGAD,EAASC,UAAW,CACpBpD,IAAIS,GAAQL,EAAwB8C,EAAK5C,IAAiBA,CAI1D,OAHG2C,KACDE,EAASrB,UAAY,IAEhBuB,EAAAA,OAAOC,EAAAA,EAAER,EAAQrC,GAAQ0C,EAAUH,OCpIxCO,EAAU,SAAAC,GAEdxD,GAAI8C,GAASU,CAkCb,QAASH,OA9BI,SACXhB,sDACa,qCACF,iCACD,2CACU,0CAIpBrC,IAAIyD,GAAWrB,GACbD,SAAAA,EACAG,OAAAA,EACAC,gBAAAA,IAEEmB,EAAS,WACX,GAAID,EAAStD,OAAS,EAAG,CACvBH,GAAIyD,GAAWrB,GACbD,SAAAA,EACAG,OAAAA,EACAC,gBAAAA,GAGF,OAAOM,GAAaC,EAAQW,EAxBvB,KAwBuCE,EAAOrD,IAGvDoD,KACA5D,SAAS8D,iBAAiB,mBAAoBF,GAC9C5D,SAAS8D,iBAAiB,OAAQF","file":"preact-habitat.js","sourcesContent":["import { h, render } from \"preact\";\n/**\n * Removes `-` fron a string and capetalize the letter after\n * example: data-props-hello-world => dataPropsHelloWorld\n * Used for props passed from host DOM element\n * @param {String} str string\n * @return {String} Capetalized string\n */\nconst camelcasize = str => {\n return str.replace(/-([a-z])/gi, (all, letter) => {\n return letter.toUpperCase();\n });\n};\n\n/**\n * [getExecutedScript internal widget to provide the currently executed script]\n * @param {document} document [Browser document object]\n * @return {HTMLElement} [script Element]\n */\nconst getExecutedScript = () => {\n return (\n document.currentScript ||\n (() => {\n let scripts = document.getElementsByTagName(\"script\");\n return scripts[scripts.length - 1];\n })()\n );\n};\n\n/**\n * Get the props from a host element's data attributes\n * @param {Element} tag The host element\n * @return {Object} props object to be passed to the component\n */\nconst collectPropsFromElement = (element, defaultProps = {}) => {\n let attrs = element.attributes;\n\n let props = Object.assign({}, defaultProps);\n\n // collect from element\n Object.keys(attrs).forEach(key => {\n if (attrs.hasOwnProperty(key)) {\n let dataAttrName = attrs[key].name;\n if (!dataAttrName || typeof dataAttrName !== \"string\") {\n return false;\n }\n let propName = dataAttrName.split(/(data-props?-)/).pop() || '';\n propName = camelcasize(propName);\n if (dataAttrName !== propName) {\n let propValue = attrs[key].nodeValue;\n props[propName] = propValue;\n }\n }\n });\n\n // check for child script text/props or application/json\n [].forEach.call(element.getElementsByTagName('script'), scrp => {\n let propsObj = {}\n if(scrp.hasAttribute('type')) {\n if (\n scrp.getAttribute(\"type\") !== \"text/props\" &&\n scrp.getAttribute(\"type\") !== \"application/json\"\n )\n return;\n try {\n propsObj = JSON.parse(scrp.innerHTML);\n } catch(e) {\n throw new Error(e)\n }\n Object.assign(props, propsObj)\n }\n }); \n\n return props;\n};\n\nconst getHabitatSelectorFromClient = (currentScript) => {\n let scriptTagAttrs = currentScript.attributes;\n let selector = null;\n // check for another props attached to the tag\n Object.keys(scriptTagAttrs).forEach(key => {\n if (scriptTagAttrs.hasOwnProperty(key)) {\n const dataAttrName = scriptTagAttrs[key].name;\n if (dataAttrName === 'data-mount-in') {\n selector = scriptTagAttrs[key].nodeValue;\n }\n }\n });\n return selector\n}\n\n/**\n * Return array of 0 or more elements that will host our widget\n * @param {id} attrId the data widget id attribute the host should have\n * @param {document} scope Docuemnt object or DOM Element as a scope\n * @return {Array} Array of matching habitats\n */\nconst widgetDOMHostElements = (\n { selector, inline, clientSpecified}\n) => {\n let hostNodes = [];\n let currentScript = getExecutedScript();\n\n if (inline === true) {\n let parentNode = currentScript.parentNode;\n hostNodes.push(parentNode);\n }\n if (clientSpecified === true && !selector) {\n // user did not specify where to mount - get it from script tag attributes\n selector = getHabitatSelectorFromClient(currentScript);\n }\n if (selector) {\n [].forEach.call(document.querySelectorAll(selector), queriedTag => {\n hostNodes.push(queriedTag);\n });\n }\n return hostNodes;\n};\n\n/**\n * preact render function that will be queued if the DOM is not ready\n * and executed immeidatly if DOM is ready\n */\nconst preactRender = (widget, hostElements, root, cleanRoot, defaultProps) => {\n hostElements.forEach(elm => {\n let hostNode = elm;\n if (hostNode._habitat) {\n return; \n }\n hostNode._habitat = true;\n let props = collectPropsFromElement(elm, defaultProps) || defaultProps;\n if(cleanRoot) {\n hostNode.innerHTML = \"\";\n }\n return render(h(widget, props), hostNode, root);\n });\n};\n\nexport {\n collectPropsFromElement,\n widgetDOMHostElements,\n getExecutedScript,\n camelcasize,\n preactRender,\n getHabitatSelectorFromClient\n};\n","import { widgetDOMHostElements, preactRender } from \"./lib\";\n\nconst habitat = Widget => {\n // Widget represents the Preact component we need to mount\n let widget = Widget;\n // preact root render helper\n let root = null;\n\n let render = (\n {\n selector = null,\n inline = false,\n clean = false,\n clientSpecified = false,\n defaultProps = {}\n } = {}\n ) => {\n let elements = widgetDOMHostElements({\n selector,\n inline,\n clientSpecified\n });\n let loaded = () => {\n if (elements.length > 0) {\n let elements = widgetDOMHostElements({\n selector,\n inline,\n clientSpecified\n });\n\n return preactRender(widget, elements, root, clean, defaultProps);\n }\n };\n loaded();\n document.addEventListener(\"DOMContentLoaded\", loaded);\n document.addEventListener(\"load\", loaded);\n };\n\n return { render };\n};\n\nexport default habitat;\n"]} -------------------------------------------------------------------------------- /dist/preact-habitat.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("preact")):"function"==typeof define&&define.amd?define(["preact"],t):e.preactHabitat=t(e.preact)}(this,function(e){var t=function(e){return e.replace(/-([a-z])/gi,function(e,t){return t.toUpperCase()})},n=function(){return document.currentScript||function(){var e=document.getElementsByTagName("script");return e[e.length-1]}()},r=function(e,n){void 0===n&&(n={});var r=e.attributes,i=Object.assign({},n);return Object.keys(r).forEach(function(e){if(r.hasOwnProperty(e)){var n=r[e].name;if(!n||"string"!=typeof n)return!1;var a=n.split(/(data-props?-)/).pop()||"";if(a=t(a),n!==a){var o=r[e].nodeValue;i[a]=o}}}),[].forEach.call(e.getElementsByTagName("script"),function(e){var t={};if(e.hasAttribute("type")){if("text/props"!==e.getAttribute("type")&&"application/json"!==e.getAttribute("type"))return;try{t=JSON.parse(e.innerHTML)}catch(e){throw new Error(e)}Object.assign(i,t)}}),i},i=function(e){var t=e.attributes,n=null;return Object.keys(t).forEach(function(e){if(t.hasOwnProperty(e)){"data-mount-in"===t[e].name&&(n=t[e].nodeValue)}}),n},a=function(e){var t=e.selector,r=e.inline,a=e.clientSpecified,o=[],c=n();if(!0===r){var u=c.parentNode;o.push(u)}return!0!==a||t||(t=i(c)),t&&[].forEach.call(document.querySelectorAll(t),function(e){o.push(e)}),o},o=function(t,n,i,a,o){n.forEach(function(n){var c=n;if(!c._habitat){c._habitat=!0;var u=r(n,o)||o;return a&&(c.innerHTML=""),e.render(e.h(t,u),c,i)}})};return function(e){var t=e;return{render:function(e){void 0===e&&(e={});var n=e.selector;void 0===n&&(n=null);var r=e.inline;void 0===r&&(r=!1);var i=e.clean;void 0===i&&(i=!1);var c=e.clientSpecified;void 0===c&&(c=!1);var u=e.defaultProps;void 0===u&&(u={});var f=a({selector:n,inline:r,clientSpecified:c}),d=function(){if(f.length>0){var e=a({selector:n,inline:r,clientSpecified:c});return o(t,e,null,i,u)}};d(),document.addEventListener("DOMContentLoaded",d),document.addEventListener("load",d)}}}}); 2 | //# sourceMappingURL=preact-habitat.umd.js.map -------------------------------------------------------------------------------- /dist/preact-habitat.umd.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/lib.js","../src/index.js"],"names":["const","camelcasize","str","replace","all","letter","toUpperCase","getExecutedScript","document","currentScript","let","scripts","getElementsByTagName","length","collectPropsFromElement","element","defaultProps","attrs","attributes","props","Object","assign","keys","forEach","key","hasOwnProperty","dataAttrName","name","propName","split","pop","propValue","nodeValue","call","scrp","propsObj","hasAttribute","getAttribute","JSON","parse","innerHTML","e","Error","getHabitatSelectorFromClient","scriptTagAttrs","selector","widgetDOMHostElements","ref","inline","clientSpecified","hostNodes","parentNode","push","querySelectorAll","queriedTag","preactRender","widget","hostElements","root","cleanRoot","elm","hostNode","_habitat","render","h","Widget","elements","loaded","clean","addEventListener"],"mappings":"iNAQAA,GAAMC,GAAc,SAAAC,GAClB,MAAOA,GAAIC,QAAQ,aAAc,SAACC,EAAKC,GACrC,MAAOA,GAAOC,iBASZC,EAAoB,WACxB,MACEC,UAASC,eACT,WACEC,GAAIC,GAAUH,SAASI,qBAAqB,SAC5C,OAAOD,GAAQA,EAAQE,OAAS,OAUhCC,EAA0B,SAACC,EAASC,qBACxCN,IAAIO,GAAQF,EAAQG,WAEhBC,EAAQC,OAAOC,UAAWL,EAoC9B,OAjCAI,QAAOE,KAAKL,GAAOM,QAAQ,SAAAC,GACzB,GAAIP,EAAMQ,eAAeD,GAAM,CAC7Bd,GAAIgB,GAAeT,EAAMO,GAAKG,IAC9B,KAAKD,GAAwC,gBAAjBA,GAC1B,OAAO,CAEThB,IAAIkB,GAAWF,EAAaG,MAAM,kBAAkBC,OAAS,EAE7D,IADAF,EAAW3B,EAAY2B,GACnBF,IAAiBE,EAAU,CAC7BlB,GAAIqB,GAAYd,EAAMO,GAAKQ,SAC3Bb,GAAMS,GAAYG,SAMrBR,QAAQU,KAAKlB,EAAQH,qBAAqB,UAAW,SAAAsB,GACtDxB,GAAIyB,KACJ,IAAGD,EAAKE,aAAa,QAAS,CAC5B,GACgC,eAA9BF,EAAKG,aAAa,SACY,qBAA9BH,EAAKG,aAAa,QAElB,MACF,KACEF,EAAWG,KAAKC,MAAML,EAAKM,WAC3B,MAAMC,GACN,KAAM,IAAIC,OAAMD,GAElBrB,OAAOC,OAAOF,EAAOgB,MAIlBhB,GAGHwB,EAA+B,SAAClC,GACpCC,GAAIkC,GAAiBnC,EAAcS,WAC/B2B,EAAW,IAUf,OARAzB,QAAOE,KAAKsB,GAAgBrB,QAAQ,SAAAC,GAClC,GAAIoB,EAAenB,eAAeD,GAAM,CAEjB,kBADAoB,EAAepB,GAAKG,OAEvCkB,EAAWD,EAAepB,GAAKQ,cAI9Ba,GASHC,EAAwB,SAC5BC,MAAEF,GAAQE,EAAAF,SAAEG,EAAMD,EAAAC,OAAEC,EAAeF,EAAAE,gBAE/BC,KACAzC,EAAgBF,GAEpB,KAAe,IAAXyC,EAAiB,CACnBtC,GAAIyC,GAAa1C,EAAc0C,UAC/BD,GAAUE,KAAKD,GAWjB,OATwB,IAApBF,GAA6BJ,IAE/BA,EAAWF,EAA6BlC,IAEtCoC,MACCtB,QAAQU,KAAKzB,SAAS6C,iBAAiBR,GAAW,SAAAS,GACnDJ,EAAUE,KAAKE,KAGZJ,GAOHK,EAAe,SAACC,EAAQC,EAAcC,EAAMC,EAAW3C,GAC3DyC,EAAalC,QAAQ,SAAAqC,GACnBlD,GAAImD,GAAWD,CACf,KAAIC,EAASC,SAAb,CAGAD,EAASC,UAAW,CACpBpD,IAAIS,GAAQL,EAAwB8C,EAAK5C,IAAiBA,CAI1D,OAHG2C,KACDE,EAASrB,UAAY,IAEhBuB,EAAAA,OAAOC,EAAAA,EAAER,EAAQrC,GAAQ0C,EAAUH,aCpI9B,UAAAO,GAEdvD,GAAI8C,GAASS,CAkCb,QAASF,OA9BI,SACXhB,sDACa,qCACF,iCACD,2CACU,0CAIpBrC,IAAIwD,GAAWpB,GACbD,SAAAA,EACAG,OAAAA,EACAC,gBAAAA,IAEEkB,EAAS,WACX,GAAID,EAASrD,OAAS,EAAG,CACvBH,GAAIwD,GAAWpB,GACbD,SAAAA,EACAG,OAAAA,EACAC,gBAAAA,GAGF,OAAOM,GAAaC,EAAQU,EAxBvB,KAwBuCE,EAAOpD,IAGvDmD,KACA3D,SAAS6D,iBAAiB,mBAAoBF,GAC9C3D,SAAS6D,iBAAiB,OAAQF","file":"preact-habitat.umd.js","sourcesContent":["import { h, render } from \"preact\";\n/**\n * Removes `-` fron a string and capetalize the letter after\n * example: data-props-hello-world => dataPropsHelloWorld\n * Used for props passed from host DOM element\n * @param {String} str string\n * @return {String} Capetalized string\n */\nconst camelcasize = str => {\n return str.replace(/-([a-z])/gi, (all, letter) => {\n return letter.toUpperCase();\n });\n};\n\n/**\n * [getExecutedScript internal widget to provide the currently executed script]\n * @param {document} document [Browser document object]\n * @return {HTMLElement} [script Element]\n */\nconst getExecutedScript = () => {\n return (\n document.currentScript ||\n (() => {\n let scripts = document.getElementsByTagName(\"script\");\n return scripts[scripts.length - 1];\n })()\n );\n};\n\n/**\n * Get the props from a host element's data attributes\n * @param {Element} tag The host element\n * @return {Object} props object to be passed to the component\n */\nconst collectPropsFromElement = (element, defaultProps = {}) => {\n let attrs = element.attributes;\n\n let props = Object.assign({}, defaultProps);\n\n // collect from element\n Object.keys(attrs).forEach(key => {\n if (attrs.hasOwnProperty(key)) {\n let dataAttrName = attrs[key].name;\n if (!dataAttrName || typeof dataAttrName !== \"string\") {\n return false;\n }\n let propName = dataAttrName.split(/(data-props?-)/).pop() || '';\n propName = camelcasize(propName);\n if (dataAttrName !== propName) {\n let propValue = attrs[key].nodeValue;\n props[propName] = propValue;\n }\n }\n });\n\n // check for child script text/props or application/json\n [].forEach.call(element.getElementsByTagName('script'), scrp => {\n let propsObj = {}\n if(scrp.hasAttribute('type')) {\n if (\n scrp.getAttribute(\"type\") !== \"text/props\" &&\n scrp.getAttribute(\"type\") !== \"application/json\"\n )\n return;\n try {\n propsObj = JSON.parse(scrp.innerHTML);\n } catch(e) {\n throw new Error(e)\n }\n Object.assign(props, propsObj)\n }\n }); \n\n return props;\n};\n\nconst getHabitatSelectorFromClient = (currentScript) => {\n let scriptTagAttrs = currentScript.attributes;\n let selector = null;\n // check for another props attached to the tag\n Object.keys(scriptTagAttrs).forEach(key => {\n if (scriptTagAttrs.hasOwnProperty(key)) {\n const dataAttrName = scriptTagAttrs[key].name;\n if (dataAttrName === 'data-mount-in') {\n selector = scriptTagAttrs[key].nodeValue;\n }\n }\n });\n return selector\n}\n\n/**\n * Return array of 0 or more elements that will host our widget\n * @param {id} attrId the data widget id attribute the host should have\n * @param {document} scope Docuemnt object or DOM Element as a scope\n * @return {Array} Array of matching habitats\n */\nconst widgetDOMHostElements = (\n { selector, inline, clientSpecified}\n) => {\n let hostNodes = [];\n let currentScript = getExecutedScript();\n\n if (inline === true) {\n let parentNode = currentScript.parentNode;\n hostNodes.push(parentNode);\n }\n if (clientSpecified === true && !selector) {\n // user did not specify where to mount - get it from script tag attributes\n selector = getHabitatSelectorFromClient(currentScript);\n }\n if (selector) {\n [].forEach.call(document.querySelectorAll(selector), queriedTag => {\n hostNodes.push(queriedTag);\n });\n }\n return hostNodes;\n};\n\n/**\n * preact render function that will be queued if the DOM is not ready\n * and executed immeidatly if DOM is ready\n */\nconst preactRender = (widget, hostElements, root, cleanRoot, defaultProps) => {\n hostElements.forEach(elm => {\n let hostNode = elm;\n if (hostNode._habitat) {\n return; \n }\n hostNode._habitat = true;\n let props = collectPropsFromElement(elm, defaultProps) || defaultProps;\n if(cleanRoot) {\n hostNode.innerHTML = \"\";\n }\n return render(h(widget, props), hostNode, root);\n });\n};\n\nexport {\n collectPropsFromElement,\n widgetDOMHostElements,\n getExecutedScript,\n camelcasize,\n preactRender,\n getHabitatSelectorFromClient\n};\n","import { widgetDOMHostElements, preactRender } from \"./lib\";\n\nconst habitat = Widget => {\n // Widget represents the Preact component we need to mount\n let widget = Widget;\n // preact root render helper\n let root = null;\n\n let render = (\n {\n selector = null,\n inline = false,\n clean = false,\n clientSpecified = false,\n defaultProps = {}\n } = {}\n ) => {\n let elements = widgetDOMHostElements({\n selector,\n inline,\n clientSpecified\n });\n let loaded = () => {\n if (elements.length > 0) {\n let elements = widgetDOMHostElements({\n selector,\n inline,\n clientSpecified\n });\n\n return preactRender(widget, elements, root, clean, defaultProps);\n }\n };\n loaded();\n document.addEventListener(\"DOMContentLoaded\", loaded);\n document.addEventListener(\"load\", loaded);\n };\n\n return { render };\n};\n\nexport default habitat;\n"]} -------------------------------------------------------------------------------- /docs/artwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zouhir/preact-habitat/97e7261955af385527e70aad648cd6f585c0285f/docs/artwork.png -------------------------------------------------------------------------------- /docs/artwork_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zouhir/preact-habitat/97e7261955af385527e70aad648cd6f585c0285f/docs/artwork_2.png -------------------------------------------------------------------------------- /docs/artworkv3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zouhir/preact-habitat/97e7261955af385527e70aad648cd6f585c0285f/docs/artworkv3.gif -------------------------------------------------------------------------------- /docs/readmev2.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | Preact Habitat 5 |
6 |

7 |
8 | 9 | Preact habitat is a 756Byte module that will help you ship your Preact components \ widget to any world wide DOM page in a very easy and neat way. 10 | 11 | ## Demos 12 | 13 | - *Simple Login* 🔑 [link](https://preact-habitat-inline.netlify.com/) 14 | 15 | - *Youtube Players* ▶️ [link](https://preact-habitat-youtube.netlify.com/) 16 | 17 | ## Use Case 18 | 19 | If you have built a [Preact](https://preactjs.com/) component (eg: Video, login, signup or booking components) and would like to bundle it and ship it to be loaded in multiple web applications, blogs without with mostly 0 or very minimal configuration from your package host, then preact-habitat is what you are after! 20 | 21 | ## Installation 22 | 23 | ```bash 24 | 25 | npm install --save preact-habitat 26 | 27 | ``` 28 | 29 | ## How to use 30 | 31 | ### Render your component using Habitat 32 | > ✨ NEW: Now habitat support multiple widget rendering✨ 33 | 34 | ```js 35 | import habitat from 'preact-habitat'; 36 | import WidgetAwesomeOne from './components/WidgetAwesome'; 37 | import WidgetAwesomeTwo from './components/WidgetAwesome'; 38 | import WidgetAwesomeThree from './components/WidgetAwesome'; 39 | 40 | let habitatOne = habitat(WidgetAwesomeOne); 41 | let habitatTwo = habitat(WidgetAwesomeTwo); 42 | let habitatThree = habitat(WidgetAwesomeTwo); 43 | 44 | habitatOne.render(); 45 | habitatTwo.render(); 46 | habitatThree.render({ name: 'data-mount-here', value: 'mount-number-three' }); 47 | ``` 48 | 49 | 50 | ### Set the build output library type to UMD 51 | 52 | usage example in Webpack: 53 | 54 | ```js 55 | output: { 56 | ... 57 | libraryTarget: 'umd' 58 | } 59 | 60 | ``` 61 | 62 | ### Inline Client Integration 63 | 64 | *Assuming your bundle available on: `https://cdn.awesome/widget.js`* 65 | 66 | ```html 67 |
68 | 69 |
70 | ``` 71 | 72 | > ✨ Pass props! ✨ 73 | 74 | ```html 75 |
76 | 77 |
78 | ``` 79 | 80 | ### Mount multiple widgets (not inline) 81 | 82 | > ✨ NEW: data-mount script attr ✨ 83 | 84 | ```html 85 | 86 |
87 |
88 | 89 |
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 | 24 |

Preact Habitat

25 |
26 | Star 27 |
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 |
22 | {!isAauthenticated 23 | ? 30 | :

Welcome

} 31 |
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 | }) --------------------------------------------------------------------------------