├── .babelrc ├── .gitignore ├── .travis.yml ├── README.md ├── SUMMARY.md ├── book.json ├── build └── gitbook.css ├── dist ├── preact-layout.min.js └── preact-layout.umd.js ├── docs ├── README.md ├── api │ ├── Layout.md │ ├── README.md │ ├── Section.md │ └── contribution-functions.md ├── contributing-guide │ └── README.md └── getting-started │ ├── Examples.md │ ├── README.md │ ├── Setup.md │ └── Usage-guide.md ├── examples ├── README.md ├── buildAll.js ├── header-footer │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── components │ │ │ ├── HomePage.jsx │ │ │ └── Layouts.jsx │ │ └── index.jsx │ └── webpack.config.js └── testAll.js ├── package.json ├── preact-layout.png ├── src └── index.jsx ├── test └── Layout.spec.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-es2015-template-literals", { "loose": true }], 4 | "transform-es2015-literals", 5 | "transform-es2015-function-name", 6 | "transform-es2015-arrow-functions", 7 | "transform-es2015-block-scoped-functions", 8 | ["transform-es2015-classes", { "loose": true }], 9 | "transform-es2015-object-super", 10 | "transform-es2015-shorthand-properties", 11 | ["transform-es2015-computed-properties", { "loose": true }], 12 | ["transform-es2015-for-of", { "loose": true }], 13 | "transform-es2015-sticky-regex", 14 | "transform-es2015-unicode-regex", 15 | "check-es2015-constants", 16 | ["transform-es2015-spread", { "loose": true }], 17 | "transform-es2015-parameters", 18 | ["transform-es2015-destructuring", { "loose": true }], 19 | "transform-es2015-block-scoping", 20 | "transform-object-rest-spread", 21 | "transform-es3-member-expression-literals", 22 | "transform-es3-property-literals", 23 | ["transform-react-jsx", { "pragma": "h" }] 24 | ], 25 | "env": { 26 | "commonjs": { 27 | "plugins": [ 28 | ["transform-es2015-modules-commonjs", { "loose": true }], 29 | ["transform-react-jsx", { "pragma": "h" }] 30 | ] 31 | }, 32 | "es": { 33 | "plugins": [ 34 | ["transform-react-jsx", { "pragma": "h" }] 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | lib 5 | es 6 | coverage 7 | _book 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | script: 7 | - npm run test 8 | - npm run build 9 | - npm run check:examples 10 | branches: 11 | only: 12 | - master 13 | - /^greenkeeper-.*$/ 14 | cache: 15 | directories: 16 | - $HOME/.npm 17 | - examples/header-footer/node_modules 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # preact-layout 2 | #### Small and simple layout library for Preact. 3 | 4 | [![npm](https://img.shields.io/npm/v/preact-layout.svg)](https://npmjs.com/package/preact-layout) 5 | [![license](https://img.shields.io/npm/l/preact-layout.svg)](https://creativecommons.org/licenses/by/4.0/) 6 | [![travis](https://img.shields.io/travis/Download/preact-layout.svg)](https://travis-ci.org/Download/preact-layout) 7 | [![greenkeeper](https://img.shields.io/david/Download/preact-layout.svg)](https://greenkeeper.io/) 8 | ![mind BLOWN](https://img.shields.io/badge/mind-BLOWN-ff69b4.svg) 9 | 10 | ![preact layout](https://cdn.rawgit.com/download/preact-layout/0.2.0/preact-layout.png) 11 | 12 | ### Think out of the box! 13 | 14 |         15 | 16 |   17 |       18 | 19 | [Preact](https://preactjs.com/) is beautiful and pure. Literally, because with 20 | Preact we mostly write pure components that take properties and render markup 21 | possibly including child components that we control via their props. Information 22 | flows one way and everything is good. 23 | 24 | But what if a component needs to render some data in another section of the page? 25 | In the header for example? Do we really have to make the root component aware of 26 | the title of each page? What if we need something in the footer as well? Or in 27 | some sidebar? We want our components to be able to contribute content to other 28 | sections of the page, even if those sections are higher up the tree... 29 | Is it even possible? 30 | 31 | preact-layout does not only make this possible, it makes it **simple**! 32 | 33 | ## Getting Started 34 | Getting started with preact-layout: 35 | * Play with the [Preact Layout Kickstart](http://codepen.io/StijnDeWitt/pen/rrzJEA?editors=0010) CodePen. 36 | * [Setup](https://download.github.io/preact-layout/docs/getting-started/Setup.html) - Add preact-layout to your project 37 | * [Basic Usage](https://download.github.io/preact-layout/docs/getting-started/Basic-usage.html) - Using Layout, Section and contribution functions 38 | * [Examples](https://download.github.io/preact-layout/docs/getting-started/Examples.html) - Learn by example! 39 | 40 | ## Why 41 | Preact Layout elegantly solves some common layout challenges you will undoubtedly 42 | encounter when using Preact, due to the hierarchical nature of the component 43 | tree and the one-way data flow. Preact Layout allows you to think out of the box. 44 | 45 | ### Simple but powerful API 46 | With just 2 components, the API is very simple to learn, yet powerful. 47 | * [Layout](docs/api/Layout.md) to define layouts 48 | * [Section](docs/api/Section.md) to divide the layout into multiple sections 49 | 50 | ### Lightweight 51 | preact-layout is microscopically small. The sources are just over 2 Kb 52 | and the minified and gzipped distribution file weighs only 1 kB. 53 | 54 | ### Well documented 55 | A good library needs plenty of good docs. In the case of preact-layout, the 56 | docs are way bigger than the library itself! We have a 57 | [Usage guide](https://download.github.io/preact-layout/docs/getting-started/Basic-usage.html), 58 | [API docs](https://download.github.io/preact-layout/docs/api/), 59 | [Examples](https://download.github.io/preact-layout/docs/getting-started/Examples.html) and a 60 | [Preact Layout Kickstart CodePen](http://codepen.io/StijnDeWitt/pen/rrzJEA?editors=0010) to learn from. 61 | 62 | ### Well tested 63 | Good tests ensure not only that the code works, but that it keeps working! We use [Travis 64 | CI](https://travis-ci.org/Download/preact-layout) to test every push to master so we 65 | catch any bugs before they make it into a release. 66 | 67 | ### Open Source, Open Community 68 | preact-layout is Open Source software with a 69 | [public code repository](https://github.com/download/preact-layout) and 70 | [public issue tracker](https://github.com/download/preact-layout/issues). 71 | 72 | ## Using preact-layout 73 | * Define contribution functions 74 | * Create a layout 75 | * Use the contribution functions in your components 76 | * Nest the component inside the layout 77 | 78 | ### Define contribution functions 79 | preact-layout allows you to define [contribution functions](https://download.github.io/preact-layout/docs/api/contribution-functions.html) 80 | that are used as JSX tags that signal that the components contained in those tags 81 | should be contributed to *another section* of the parent layout. 82 | 83 | For example, for a simple layout with a header and a footer around some main 84 | content block, we might define these functions: 85 | 86 | ```js 87 | function Header(){} 88 | function Footer(){} 89 | ``` 90 | 91 | ### Create a layout 92 | Create a layout with sections for the Header and Footer: 93 | 94 | ```js 95 | function MyLayout({children}) {return ( 96 | 97 |
98 |
99 |
header
100 |
101 | 102 |
{children}
103 | 104 | 107 |
108 |
109 | )} 110 | ``` 111 | 112 | ### Use the contribution functions in your components 113 | Now, we can build components that use the contribution functions to contribute 114 | components to the related sections of the layout: 115 | 116 | ```js 117 | function MyPage(){return ( 118 |
119 |

my page

120 |
main article
121 | 122 |
123 | )} 124 | ``` 125 | 126 | ### Nest the component inside the layout 127 | Finally, render the component nested within the layout: 128 | 129 | ```js 130 | render( 131 | 132 | 133 | 134 | , 135 | document.getElementsByTagName('body')[0] 136 | ) 137 | ``` 138 | 139 | This will result in: 140 | 141 | ```html 142 |
143 |
144 |

my page

145 |
146 |
147 |
main article
148 |
149 | 152 |
153 | ``` 154 | Read more in the [Usage Guide](https://download.github.io/preact-layout/docs/getting-started/Usage-guide.html). 155 | 156 | ## Component Reference 157 | * [Layout](https://download.github.io/preact-layout/docs/api/Layout.html) - Create layouts using `Layout` 158 | * [Section](https://download.github.io/preact-layout/docs/api/Section.html) - Divide your layout into named `Section`s 159 | * [contribution functions](https://download.github.io/preact-layout/docs/api/contribution-functions.html) - Defining conribution functions 160 | 161 | ## Performance considerations 162 | When collecting contributions, preact-layout peeks ahead at the elements that 163 | the child components will produce by rendering those child components. The 164 | resulting elements could itself again contain components, which would need to 165 | be rendered as well, leading to recursive rendering that of course comes at a 166 | performance cost. You can tune this cost by setting the 167 | [recurse](https://download.github.io/preact-layout/docs/api/Layout.html#recurse) 168 | attribute on the `Layout` to some positive number. It defaults to `9`. 169 | 170 | ## Issues 171 | Add an issue in this project's [issue tracker](https://github.com/download/preact-layout/issues) 172 | to let me know of any problems you find, or questions you may have. 173 | 174 | ## Contributing 175 | This project welcomes contributions from the community! 176 | Learn more in the [contributing guide](https://download.github.io/preact-layout/docs/contributing-guide/). 177 | 178 | ## Copyright 179 | Copyright 2016 by [Stijn de Witt](http://StijnDeWitt.com). Some rights reserved. 180 | 181 | ## License 182 | Licensed under the [Creative Commons Attribution 4.0 International (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) Open Source license. 183 | 184 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | * [Getting Started](/docs/getting-started/README.md) 4 | * [Setup](/docs/getting-started/Setup.md) 5 | * [Usage Guide](/docs/getting-started/Usage-guide.md) 6 | * [Examples](/docs/getting-started/Examples.md) 7 | * [Component Reference](/docs/api/README.md) 8 | * [Layout](/docs/api/Layout.md) 9 | * [Section](/docs/api/Section.md) 10 | * [contribution functions](/docs/api/contribution-functions.md) 11 | * [Contributing Guide](/docs/contributing-guide/README.md) 12 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.x.x", 3 | "title": "preact-layout", 4 | "plugins": ["prism", "-highlight", "github", "anchorjs"], 5 | "pluginsConfig": { 6 | "github": { 7 | "url": "https://github.com/Download/preact-layout/" 8 | }, 9 | "theme-default": { 10 | "showLevel": true, 11 | "styles": { 12 | "website": "build/gitbook.css" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build/gitbook.css: -------------------------------------------------------------------------------- 1 | .book-summary ul.summary li span { 2 | cursor: not-allowed; 3 | opacity: 0.3; 4 | } 5 | 6 | .book-summary ul.summary li a:hover { 7 | color: #008cff; 8 | text-decoration: none; 9 | } 10 | -------------------------------------------------------------------------------- /dist/preact-layout.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t(require("preact"));else if("function"==typeof define&&define.amd)define(["preact"],t);else{var n=t("object"==typeof exports?require("preact"):e.preact);for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}(this,function(e){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e,t){var n={};for(var r in e)t.indexOf(r)<0&&Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function o(e,t){var n=e.className,o=e.recurse,i=e.children,u=(r(e,["className","recurse","children"]),a(i)),d=u.main,f=u.sections;return c(d,f,s({},t),o),i&&1===i.length?i[0]:(0,p.h)("div",{className:n||"Layout"},i)}function i(e,t){var n=e.children,o=r(e,["type","children"]);return n&&1===n.length?n[0]:(0,p.h)("div",o,n)}function a(e,t){t||(t={sections:[]}),e.nodeName===i&&(e.attributes&&e.attributes.type?t.sections.push(e):t.main=e);var n=Array.isArray(e)?e:e.children;return n&&n.forEach(function(e){a(e,t)}),t}function c(e,t,n,r,o,i){var a=[],p=!i;return n=n||{},void 0===r&&(r=9),i=i||{},t.forEach(function(e){return i[e.attributes.type]=i[e.attributes.type]||e.children||[]}),e&&e.children&&e.children.forEach(function(e){if(u(e,t))return i[e.nodeName]||(i[e.nodeName]=[]),void(e.attributes&&e.attributes.append?i[e.nodeName].push.apply(i[e.nodeName],e.children||[]):e.attributes&&e.attributes.prepend?i[e.nodeName].unshift.apply(i[e.nodeName],e.children||[]):i[e.nodeName]=e.children||[]);if(a.push(e),"function"==typeof e.nodeName&&r){var p=s({},e.nodeName.defaultProps,e.attributes,{children:e.children});if(e.nodeName.prototype&&"function"==typeof e.nodeName.prototype.render){var d=new e.nodeName(p,n);d.props=p,d.context=n,d.componentWillMount&&d.componentWillMount(),e=d.render(d.props,d.state,d.context),d.getChildContext&&(n=s({},n,d.getChildContext()))}else e=e.nodeName(p,n);r--}c(e,t,n,r,o,i)}),o||(e.children&&(e.children=a),p&&t.forEach(function(e){return e.children=i[e.attributes.type]})),i}function u(e,t){return t.filter(function(t){return e.nodeName===t.attributes.type}).length>0}t.__esModule=!0,t.processNode=t.isContribution=t.getSections=t.Section=t.Layout=void 0;var s=Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},p=n(1);t.Layout=o,t.Section=i,t.getSections=a,t.isContribution=u,t.processNode=c},function(t,n){t.exports=e}])}); -------------------------------------------------------------------------------- /dist/preact-layout.umd.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("preact")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["preact"], factory); 6 | else { 7 | var a = typeof exports === 'object' ? factory(require("preact")) : factory(root["preact"]); 8 | for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; 9 | } 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | exports.__esModule = true; 60 | exports.processNode = exports.isContribution = exports.getSections = exports.Section = exports.Layout = undefined; 61 | 62 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 63 | 64 | var _preact = __webpack_require__(1); 65 | 66 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } 67 | 68 | function Layout(_ref, context) { 69 | var className = _ref.className; 70 | var recurse = _ref.recurse; 71 | var children = _ref.children; 72 | 73 | var props = _objectWithoutProperties(_ref, ['className', 'recurse', 'children']); 74 | 75 | var _getSections = getSections(children); 76 | 77 | var main = _getSections.main; 78 | var sections = _getSections.sections; 79 | 80 | processNode(main, sections, _extends({}, context), recurse); 81 | return children && children.length === 1 ? children[0] : (0, _preact.h)( 82 | 'div', 83 | { className: className || 'Layout' }, 84 | children 85 | ); 86 | } 87 | 88 | function Section(_ref2, context) { 89 | var type = _ref2.type; 90 | var children = _ref2.children; 91 | 92 | var props = _objectWithoutProperties(_ref2, ['type', 'children']); 93 | 94 | return children && children.length === 1 ? children[0] : (0, _preact.h)( 95 | 'div', 96 | props, 97 | children 98 | ); 99 | } 100 | 101 | function getSections(n, result) { 102 | if (!result) result = { sections: [] }; 103 | if (n.nodeName === Section) { 104 | if (n.attributes && n.attributes.type) result.sections.push(n);else result.main = n; 105 | } 106 | var children = Array.isArray(n) ? n : n.children; 107 | children && children.forEach(function (c) { 108 | getSections(c, result); 109 | }); 110 | return result; 111 | } 112 | 113 | function processNode(node, sections, context, recurse, collectOnly, results) { 114 | var leftovers = [], 115 | postProcess = !results; 116 | context = context || {}; 117 | if (recurse === undefined) recurse = 9; 118 | results = results || {}; 119 | sections.forEach(function (s) { 120 | return results[s.attributes.type] = results[s.attributes.type] || s.children || []; 121 | }); 122 | node && node.children && node.children.forEach(function (n) { 123 | if (isContribution(n, sections)) { 124 | if (!results[n.nodeName]) results[n.nodeName] = []; 125 | if (n.attributes && n.attributes.append) results[n.nodeName].push.apply(results[n.nodeName], n.children || []);else if (n.attributes && n.attributes.prepend) results[n.nodeName].unshift.apply(results[n.nodeName], n.children || []);else results[n.nodeName] = n.children || []; 126 | return; // continue 127 | } 128 | leftovers.push(n); 129 | if (typeof n.nodeName == 'function' && recurse) { 130 | var props = _extends({}, n.nodeName.defaultProps, n.attributes, { children: n.children }); 131 | if (n.nodeName.prototype && typeof n.nodeName.prototype.render == 'function') { 132 | var rn = void 0, 133 | c = new n.nodeName(props, context); 134 | c.props = props; 135 | c.context = context; 136 | if (c.componentWillMount) c.componentWillMount(); 137 | n = c.render(c.props, c.state, c.context); 138 | if (c.getChildContext) context = _extends({}, context, c.getChildContext()); 139 | } else n = n.nodeName(props, context); 140 | recurse--; 141 | } 142 | processNode(n, sections, context, recurse, collectOnly, results); 143 | }); 144 | if (!collectOnly) { 145 | if (node.children) node.children = leftovers; 146 | if (postProcess) sections.forEach(function (s) { 147 | return s.children = results[s.attributes.type]; 148 | }); 149 | } 150 | return results; 151 | } 152 | 153 | function isContribution(n, sections) { 154 | return sections.filter(function (s) { 155 | return n.nodeName === s.attributes.type; 156 | }).length > 0; 157 | } 158 | 159 | exports.Layout = Layout; 160 | exports.Section = Section; 161 | exports.getSections = getSections; 162 | exports.isContribution = isContribution; 163 | exports.processNode = processNode; 164 | 165 | /***/ }, 166 | /* 1 */ 167 | /***/ function(module, exports) { 168 | 169 | module.exports = __WEBPACK_EXTERNAL_MODULE_1__; 170 | 171 | /***/ } 172 | /******/ ]) 173 | }); 174 | ; -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | * [Getting Started](/docs/getting-started/README.md) 4 | * [Setup](/docs/getting-started/Setup.md) 5 | * [Usage Guide](/docs/getting-started/Usage-guide.md) 6 | * [Examples](/docs/getting-started/Examples.md) 7 | * [Component Reference](/docs/api/README.md) 8 | * [Layout](/docs/api/Layout.md) 9 | * [Section](/docs/api/Section.md) 10 | * [contribution functions](/docs/api/contribution-functions.md) 11 | * [Contributing Guide](/docs/contributing-guide/README.md) 12 | -------------------------------------------------------------------------------- /docs/api/Layout.md: -------------------------------------------------------------------------------- 1 | # Layout 2 | 3 | A preact-layout `Layout`: 4 | * Defines a layout with exactly one 'main' section and zero or more sections that are associated with a contribution function. 5 | * Collects layout *contributions* from the children of the main section and renders those contributions in the associated other sections of the layout 6 | * Recursively renders children of the main section to discover contributions. 7 | * By default recurses 9 levels deep 8 | * Allows the recursion level to be specified as a component attribute 9 | 10 | ## Attributes 11 | 12 | ### recurse 13 | The maximum recursion depth. Optional. Number. 14 | Defaults to `9` 15 | 16 | ## See also 17 | * [Section](Section.md) 18 | * [contribution functions](contribution-functions.md) 19 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | ## Component Reference 2 | 3 | * [Layout](Layout.md) - Create layouts using `Layout` 4 | * [Section](Section.md) - Divide your layout into named `Section`s 5 | * [contribution functions](contribution-functions.md) - Are the mutual contract between layouts and components 6 | 7 | -------------------------------------------------------------------------------- /docs/api/Section.md: -------------------------------------------------------------------------------- 1 | # Section 2 | 3 | A preact-layout `Section`: 4 | * Defines where components will be rendered within a parent `Layout` 5 | * Has a `type` attribute to associate it with a contribution function, or 6 | * Is a 'main' section (no `type` attribute) 7 | 8 | ## Attributes 9 | ### type 10 | The contribution function this section should be associated with 11 | Optional. Function. Defaults to `undefined` 12 | 13 | ## Example 14 | ```js 15 |
16 | This markup will be used when no contributions are found 17 |
18 | ``` 19 | 20 | ## See also 21 | * [Layout](Layout.md) 22 | * [contribution functions](contribution-functions.md) 23 | -------------------------------------------------------------------------------- /docs/api/contribution-functions.md: -------------------------------------------------------------------------------- 1 | # Contribution functions 2 | 3 | Contribution functions act a bit like ES6 symbols, or 'tagging interfaces' in 4 | Java speak. They don't do anything themselves, but allow a component to interact 5 | with a layout without being tighly coupled with it. 6 | 7 | In the most basic scenario, we define the contribution functions right along 8 | our layout and also import them from there in our components: 9 | 10 | *Layout.js* 11 | ```js 12 | function Header(){} 13 | function Footer(){} 14 | 15 | function MyLayout(){ 16 | // use Header and Footer here 17 | } 18 | 19 | export { 20 | Header, 21 | Footer, 22 | MyLayout 23 | } 24 | ``` 25 | 26 | *Component.js* 27 | ```js 28 | import { Header, Footer } from './Layout' 29 | 30 | function MyComponent(){ 31 | // use Header and Footer here 32 | } 33 | ``` 34 | 35 | If our site has many layouts and many components contributing to those 36 | layouts, it will probably pay off to place the contribution functions in a 37 | separate file and reuse them where needed: 38 | 39 | *Sections.js* 40 | ```js 41 | function Header(){} 42 | function Footer(){} 43 | 44 | export { 45 | Header, 46 | Footer 47 | } 48 | ``` 49 | 50 | *Layout.js* 51 | ```js 52 | import { Header, Footer } from './Sections' 53 | 54 | function MyLayout(){ 55 | // use Header and Footer here 56 | } 57 | export { 58 | MyLayout 59 | } 60 | ``` 61 | 62 | *Component.js* 63 | ```js 64 | import { Header, Footer } from './Sections' 65 | 66 | function MyComponent(){ 67 | // use Header and Footer here 68 | } 69 | ``` 70 | 71 | ## Attributes 72 | ### append 73 | When set, any contributions found will be `appended` to any existing 74 | default elements / prior contributions, instead of replacing them (default) 75 | 76 | ### prepend 77 | When set, any contributions found will be `prepended` before any existing 78 | default elements / prior contributions, instead of replacing them (default) 79 | 80 | ## Example 81 | *(assuming `Header` is a contribution function)* 82 | ```js 83 | function MyComponent(){return ( 84 |
85 |
86 | This will be appended to the header section instead of replacing it 87 |
88 | 89 |

Stuff here ends up in the main section

90 |
91 | )} 92 | ``` 93 | 94 | ## See also 95 | * [Layout](Layout.md) 96 | * [Section](Section.md) 97 | -------------------------------------------------------------------------------- /docs/contributing-guide/README.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for considering to contribute to preact-layout! For small contributions, 4 | go right ahead and create a PR following the instructions below. For bigger 5 | contributions, consider [creating an issue](https://github.com/download/preact-layout/issues/new) 6 | first, so we can discuss. 7 | 8 | ## Preparing a Pull Request 9 | Below are the general steps to create a PR: 10 | 11 | ### Fork 12 | Fork this repo on Github, then clone the fork to your local machine and install 13 | the dependencies: 14 | 15 | ```sh 16 | git clone https://github.com/your-username/preact-layout.git 17 | cd preact-layout 18 | npm install 19 | ``` 20 | 21 | ### Build and test 22 | Before we start changing stuff, let's build everything and run the tests to make 23 | sure that everything is still fine. 24 | 25 | #### Build all 26 | Running the `build` task will create both a CommonJS and a UMD build. 27 | ```sh 28 | npm run build 29 | ``` 30 | 31 | #### Build only CommonJS 32 | To create just a CommonJS build: 33 | ```sh 34 | npm run build:lib 35 | ``` 36 | The result will be in the `lib` folder. 37 | 38 | #### Build only UMD 39 | To create just a UMD build: 40 | ```sh 41 | npm run build:umd 42 | npm run build:umd:min 43 | ``` 44 | The result will be in the `dist` folder. 45 | 46 | #### Run the tests 47 | ```sh 48 | npm run test 49 | ``` 50 | To continuously watch and run tests, run the following: 51 | ```sh 52 | npm run test:watch 53 | ``` 54 | 55 | ### Create and switch to a new branch 56 | Before starting work, create a new branch based on `master` and switch to it. 57 | This will greatly simplify merging the PR later. 58 | 59 | ### Write tests 60 | It's good practice to write a (couple of) test(s) that puts your new code through the motions. 61 | 62 | ### Write the code 63 | With your new tests in place, write the code that makes them pass. 64 | 65 | ### Write documentation 66 | Remember, if it's not documented, it might as well not exist. Any new features 67 | should get a section in the documentation explaining it. 68 | 69 | #### Installing Gitbook 70 | To install the latest version of `gitbook` and prepare to build the documentation, run the following: 71 | ```sh 72 | npm run docs:prepare 73 | ``` 74 | 75 | #### Building the Docs 76 | To build the documentation, run the following: 77 | ```sh 78 | npm run docs:build 79 | ``` 80 | To watch and rebuild documentation when changes occur, run the following: 81 | ```sh 82 | npm run docs:watch 83 | ``` 84 | The docs will be served at [http://localhost:4000](http://localhost:4000). 85 | 86 | #### Publishing the Docs 87 | To publish the documentation, run the following: 88 | ```sh 89 | npm run docs:publish 90 | ``` 91 | 92 | #### Cleaning the Docs 93 | To remove previously built documentation, run the following: 94 | ```sh 95 | npm run docs:clean 96 | ``` 97 | 98 | ### Examples 99 | preact-layout comes with [official examples](http://download.github.io/preact-layout/docs/introduction/Examples.html) to demonstrate various concepts and best practices. 100 | 101 | When adding a new example, please adhere to the style and format of the existing examples, and try to reuse as much code as possible. For example, `index.html`, `server.js`, and `webpack.config.js` can typically be reused. 102 | 103 | #### Building and Testing the Examples 104 | To build and test the official preact-layout examples, run the following: 105 | ```sh 106 | npm run build:examples 107 | npm run test:examples 108 | ``` 109 | 110 | Please visit the [Examples page](http://download.github.io/preact-layout/docs/introduction/Examples.html) for information on running individual examples. 111 | 112 | Thanks for contributing! 113 | -------------------------------------------------------------------------------- /docs/getting-started/Examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | preact-layout contains a few [examples](https://github.com/download/preact-layout/tree/master/examples). 4 | 5 | ## Header/Footer Example 6 | 7 | Learn how to create a layout with a header, a main body and a footer. What site can do without it?! 8 | [Header/Footer](https://github.com/download/preact-layout/tree/master/examples/header-footer) 9 | 10 | ```sh 11 | git clone https://github.com/download/preact-layout.git 12 | 13 | cd preact-layout/examples/header-footer 14 | npm install 15 | npm start 16 | ``` 17 | open [http://localhost:8888/](http://localhost:8888/) 18 | -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | Getting started with preact-layout! 4 | 5 | * Play with the [Preact Layout Kickstart CodePen](http://codepen.io/StijnDeWitt/pen/rrzJEA?editors=0010). 6 | * [Setup](Setup.md) - Add preact-layout to your project 7 | * [Usage guide](Usage-guide.md) - Using Layout, Section and contribution functions 8 | * [Examples](Examples.md) - Learn by example! 9 | 10 | -------------------------------------------------------------------------------- /docs/getting-started/Setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Install 4 | ```sh 5 | npm install --save preact-layout 6 | ``` 7 | 8 | ## Require 9 | ```js 10 | var Layout = require('preact-layout').Layout 11 | var Section = require('preact-layout').Section 12 | ``` 13 | 14 | ## Import 15 | ```js 16 | import { Layout, Section } from 'preact-layout' 17 | ``` 18 | 19 | ## AMD 20 | ```js 21 | define(['preact-layout'], function(preactLayout){ 22 | const { Layout, Section } = preactLayout 23 | }) 24 | ``` 25 | 26 | ## Script tag 27 | ```html 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/getting-started/Usage-guide.md: -------------------------------------------------------------------------------- 1 | # Usage Guide 2 | 3 | Using preact-layout requires you to: 4 | * Define contribution functions 5 | * Create a layout referncing these contribution functions 6 | * Make components that use the contribution functions 7 | * Learn about caveats 8 | 9 | ## Define contribution functions 10 | Define *contribution functions* to represent the different sections 11 | of your website / app: 12 | ```js 13 | function Header(){} 14 | function Footer(){} 15 | ``` 16 | These functions's don't do anything in and of themselves, but they serve as 17 | symbols that can be used by both layouts and regular components. 18 | 19 | ## Create a layout 20 | Use `Layout` to create a layout which is subdivided in sections. Use `Section` 21 | to create those sections. There should be exactly one main section (not associated 22 | with a contribution function) and any `children` should be rendered inside this main section. 23 | In addition, layouts can have zero or more extra sections, each of which must be 24 | associated with a different contribution function, using the `type` attribute. 25 | 26 | Have a look at this example: 27 | 28 | ```js 29 | function MyLayout({children}) { 30 | return ( 31 | 32 |
33 |

Default title

34 |
35 | 36 |
{children}
37 | 38 | 41 |
42 | ) 43 | } 44 | ``` 45 | 46 | Much simpler than it sounded right? But here's the cool trick: Any children 47 | of the main section will be rendered before the sections themselves and if they 48 | produce *layout contributions*, those contributions will be moved to the correct 49 | section based on the associated contribution function. 50 | 51 | 52 | ## Make components that use the contribution functions 53 | Again have a look at an example: 54 | 55 | ```js 56 | function HomePage({children}) { 57 | return ( 58 |
59 |

This is the homepage!

60 | 61 |

And thats how preact-layout works. It takes the contributions made by the 62 | children and shows them in the correct region of the layout.

63 | 64 | 65 |
66 | ) 67 | } 68 | ``` 69 | As you can see, in our components, we wrap the content that we want to 70 | contribute to other sections of the layout in JSX tags created from our 71 | contribution functions. 72 | 73 | The magic happens when we combine the layout and the page we just created: 74 | 75 | ```js 76 | function Website({children, ...props}) { 77 | return ( 78 | 79 | 80 | 81 | ) 82 | } 83 | ``` 84 | 85 | `Website` will render this output: 86 | 87 | ```html 88 |
89 |
90 |

This is the homepage!

91 |
92 |
93 |

And thats how preact-layout works. It takes the contributions made by the 94 | children and shows them in the correct region of the layout.

95 |
96 | 99 |
100 | ``` 101 | 102 | Because we separated the layout concerns from the `HomePage` component, we 103 | can easily make an alternative layout and use that instead: 104 | 105 | ```js 106 | function ReversedLayout({children}) { 107 | return ( 108 | 109 | 112 | 113 |
{children}
114 | 115 |
116 |

Default title

117 |
118 |
119 | ) 120 | } 121 | ``` 122 | 123 | Change `Website` to use the alternative layout and you're done: 124 | 125 | ```js 126 | function Website({children, ...props}) { 127 | return ( 128 | 129 | 130 | 131 | ) 132 | } 133 | ``` 134 | 135 | The contributions made by `HomePage` will automatically land in the correct place: 136 | 137 | ```html 138 |
139 | 142 |
143 |

And thats how preact-layout works. It takes the contributions made by the 144 | children and shows them in the correct region of the layout.

145 |
146 |
147 |

This is the homepage!

148 |
149 |
150 | ``` 151 | 152 | ## Caveats 153 | In the examples above we used stateless functional components. 154 | But preact-layout can be used with class components just as well: 155 | 156 | ```js 157 | class HomePage extends Component { 158 | render({children}) {return ( 159 |
160 |

This is the homepage!

161 | 162 |

And thats how preact-layout works. It takes the contributions made by the 163 | children and shows them in the correct region of the layout.

164 | 165 | 166 |
167 | )} 168 | } 169 | ``` 170 | 171 | This works just as great. But things become a bit more tricky when 172 | we start using *stateful* class components. Consider this example: 173 | 174 | ```js 175 | class HomePage extends Component { 176 | state = {text: 'Hello, world!'}; 177 | 178 | render({children}, {text}) {return ( 179 |
180 |

{text}

181 | 182 |

This paragraph is in the main section

183 | 184 | 185 | 186 |
187 | )} 188 | } 189 | ``` 190 | 191 | This will render fine and appears to work, until we change the text 192 | in the input. The changes will not be reflected in the header. The reason 193 | for this is that the children of the `
` tag are actually moved 194 | outside of the `HomePage` component by the layout, so Preact does not 195 | re-render them in response to state changes. 196 | 197 | The children of the `HomePage` that are not contributed to other sections 198 | will respond to state changes as expected. Also, you are free to 199 | contribute stateful components. It's just that inside the component 200 | making the contributions, local state is not accessible in the 201 | contributed sections. 202 | 203 | So either make components that contribute markup to the layout stateless, 204 | or take care to only make use of state in the main section of the 205 | component and not in those sections that are contributed to the layout. 206 | 207 | The [Preact Layout Kickstart CodePen](http://codepen.io/StijnDeWitt/pen/rrzJEA) actually demonstrates this. 208 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Read the descriptions for every example on the [Examples](https://download.github.io/preact-layout/docs/getting-started/Examples.html) documentation page. 4 | -------------------------------------------------------------------------------- /examples/buildAll.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var { spawnSync } = require('child_process') 4 | 5 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 6 | return fs.statSync(path.join(__dirname, file)).isDirectory() 7 | }) 8 | 9 | // Ordering is important here. `npm install` must come first. 10 | var cmdArgs = [ 11 | { cmd: 'npm', args: [ 'install' ] }, 12 | { cmd: 'webpack', args: [ 'index.js' ] } 13 | ] 14 | 15 | for (const dir of exampleDirs) { 16 | for (const cmdArg of cmdArgs) { 17 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 18 | const opts = { 19 | cwd: path.join(__dirname, dir), 20 | stdio: 'inherit' 21 | } 22 | let result = {} 23 | if (process.platform === 'win32') { 24 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 25 | } else { 26 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 27 | } 28 | if (result.status !== 0) { 29 | throw new Error('Building examples exited with non-zero') 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/header-footer/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2" 5 | ], 6 | "plugins": [ 7 | ["transform-react-jsx", { "pragma": "h" }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/header-footer/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/header-footer/README.md: -------------------------------------------------------------------------------- 1 | # Header / Footer Example 2 | 3 | This example demonstrates a layout with a header and a footer that are contributed by the children. 4 | 5 | ## Running the example 6 | 7 | In the project folder: 8 | 9 | ```sh 10 | $ npm start 11 | ``` 12 | 13 | Runs the app in development mode. 14 | 15 | Open [http://localhost:8888](http://localhost:8888) to view it in the browser. 16 | 17 | -------------------------------------------------------------------------------- /examples/header-footer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "header-footer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "babel-core": "^6.16.0", 7 | "babel-loader": "^6.2.5", 8 | "babel-plugin-transform-react-jsx": "^6.8.0", 9 | "babel-preset-es2015": "^6.16.0", 10 | "babel-preset-stage-2": "^6.16.0", 11 | "webpack": "^1.13.2", 12 | "webpack-dev-server": "^1.16.1" 13 | }, 14 | "dependencies": { 15 | "preact": "^6.1.0", 16 | "preact-layout": "^0.2.0" 17 | }, 18 | "scripts": { 19 | "start": "webpack-dev-server --output-path public --output-filename bundle.js \"./src\" --content-base public --port 8888" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/header-footer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Header/Footer Example - preact-layout 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/header-footer/src/components/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { Header, Footer } from './Layouts' 3 | 4 | function HomePage({children}) {return ( 5 |
6 |
7 |

HomePage

8 |
9 | 10 |

This is the homepage. Isnt it cool?

11 | 12 |
13 | Thanks for your interest in preact-layout! 14 |
15 |
16 | )} 17 | 18 | export default HomePage 19 | -------------------------------------------------------------------------------- /examples/header-footer/src/components/Layouts.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { Layout, Section } from 'preact-layout' 3 | 4 | function Header(){} 5 | 6 | function Footer(){} 7 | 8 | function MyLayout({children}) {return ( 9 | 10 |
11 |

Default title

12 |
13 | 14 |
15 | {children} 16 |
17 | 18 |
19 |
Default footer here!! :)
20 |
21 |
22 | )} 23 | 24 | function ReversedLayout({children}) {return ( 25 | 26 |
27 |
Default footer here!! :)
28 |
29 | 30 |
31 | {children} 32 |
33 | 34 |
35 |

Default title

36 |
37 |
38 | )} 39 | 40 | export { 41 | Header, 42 | Footer, 43 | MyLayout, 44 | ReversedLayout 45 | } 46 | -------------------------------------------------------------------------------- /examples/header-footer/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact' 2 | import { MyLayout } from './components/Layouts' 3 | import HomePage from './components/HomePage' 4 | 5 | render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ) 11 | -------------------------------------------------------------------------------- /examples/header-footer/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack') 4 | 5 | var env = process.env.NODE_ENV 6 | var config = { 7 | resolve: { 8 | // IMPORTANT: Setting this option will override the default, meaning that webpack 9 | // will no longer try to resolve modules using the default extensions. If you want 10 | // modules that were required with their extension (e.g. require('./somefile.ext')) 11 | // to be properly resolved, you must include an empty string in your array. 12 | // Similarly, if you want modules that were required without extensions (e.g. 13 | // require('underscore')) to be resolved to files with “.js” extensions, you must 14 | // include ".js" in your array. 15 | // Default: ["", ".webpack.js", ".web.js", ".js"] 16 | // https://webpack.github.io/docs/configuration.html#resolve-extensions 17 | extensions: ['', '.webpack.js', '.web.js', '.js', '.json', '.jsx'], 18 | }, 19 | module: { 20 | loaders: [ 21 | { test: /\.jsx$/, loaders: ['babel-loader'], exclude: /node_modules/ } 22 | ] 23 | }, 24 | output: { 25 | libraryTarget: 'umd' 26 | }, 27 | plugins: [ 28 | new webpack.optimize.OccurrenceOrderPlugin(), 29 | new webpack.DefinePlugin({ 30 | 'process.env.NODE_ENV': JSON.stringify(env) 31 | }) 32 | ] 33 | }; 34 | 35 | module.exports = config 36 | -------------------------------------------------------------------------------- /examples/testAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var { spawnSync } = require('child_process') 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'npm', args: [ 'test' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | 27 | let result = {} 28 | if (process.platform === 'win32') { 29 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 30 | } else { 31 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 32 | } 33 | if (result.status !== 0) { 34 | throw new Error('Building examples exited with non-zero') 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-layout", 3 | "version": "1.0.2", 4 | "description": "Small and simple layout library for preact", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "files": [ 9 | "dist", 10 | "lib", 11 | "es", 12 | "src" 13 | ], 14 | "scripts": { 15 | "clean": "rimraf lib dist es coverage", 16 | "test": "cross-env BABEL_ENV=commonjs jest", 17 | "test:watch": "npm test -- --watch", 18 | "test:cov": "npm test -- --coverage", 19 | "test:examples": "babel-node examples/testAll.js", 20 | "check:examples": "npm run build:examples && npm run test:examples", 21 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 22 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 23 | "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack src/index.jsx dist/preact-layout.umd.js --display-modules --display-reasons --display-chunks", 24 | "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack src/index.jsx dist/preact-layout.min.js", 25 | "build:examples": "babel-node examples/buildAll.js", 26 | "build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min", 27 | "prepublish": "npm run clean && npm run build && npm run test && check-es3-syntax lib/ dist/ --kill --print", 28 | "docs:clean": "rimraf _book", 29 | "docs:prepare": "gitbook install", 30 | "docs:build": "npm run docs:prepare && gitbook build -g Download/preact-layout", 31 | "docs:watch": "npm run docs:prepare && gitbook serve", 32 | "docs:publish": "npm run docs:clean && npm run docs:build && cd _book && git init && git commit --allow-empty -m \"update book\" && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am \"update book\" && git push git@github.com:Download/preact-layout gh-pages --force" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/download/preact-layout.git" 37 | }, 38 | "keywords": [ 39 | "preact", 40 | "layout", 41 | "template", 42 | "1kB", 43 | "Microscopically small", 44 | "tiny", 45 | "small", 46 | "lay-out", 47 | "jsx" 48 | ], 49 | "author": "Stijn de Witt (https://StijnDeWitt.com)", 50 | "license": "CC-BY-4.0", 51 | "bugs": { 52 | "url": "https://github.com/download/preact-layout/issues" 53 | }, 54 | "homepage": "http://download.github.io/preact-layout", 55 | "dependencies": { 56 | "preact": "^6.3.0", 57 | "preact-render-to-string": "^3.2.1" 58 | }, 59 | "devDependencies": { 60 | "babel-cli": "^6.16.0", 61 | "babel-core": "^6.17.0", 62 | "babel-jest": "^16.0.0", 63 | "babel-loader": "^6.2.0", 64 | "babel-plugin-check-es2015-constants": "^6.3.13", 65 | "babel-plugin-transform-es2015-arrow-functions": "^6.3.13", 66 | "babel-plugin-transform-es2015-block-scoped-functions": "^6.3.13", 67 | "babel-plugin-transform-es2015-block-scoping": "^6.3.13", 68 | "babel-plugin-transform-es2015-classes": "^6.3.13", 69 | "babel-plugin-transform-es2015-computed-properties": "^6.3.13", 70 | "babel-plugin-transform-es2015-destructuring": "^6.16.0", 71 | "babel-plugin-transform-es2015-for-of": "^6.3.13", 72 | "babel-plugin-transform-es2015-function-name": "^6.3.13", 73 | "babel-plugin-transform-es2015-literals": "^6.3.13", 74 | "babel-plugin-transform-es2015-modules-commonjs": "^6.16.0", 75 | "babel-plugin-transform-es2015-object-super": "^6.3.13", 76 | "babel-plugin-transform-es2015-parameters": "^6.17.0", 77 | "babel-plugin-transform-es2015-shorthand-properties": "^6.3.13", 78 | "babel-plugin-transform-es2015-spread": "^6.3.13", 79 | "babel-plugin-transform-es2015-sticky-regex": "^6.3.13", 80 | "babel-plugin-transform-es2015-template-literals": "^6.3.13", 81 | "babel-plugin-transform-es2015-unicode-regex": "^6.3.13", 82 | "babel-plugin-transform-es3-member-expression-literals": "^6.5.0", 83 | "babel-plugin-transform-es3-property-literals": "^6.5.0", 84 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 85 | "babel-plugin-transform-react-jsx": "^6.8.0", 86 | "babel-register": "^6.16.3", 87 | "check-es3-syntax-cli": "^0.1.1", 88 | "cross-env": "^3.1.1", 89 | "gitbook-cli": "^2.3.0", 90 | "glob": "^7.1.1", 91 | "jest": "^17.0.0", 92 | "loose-envify": "^1.1.0", 93 | "preact-render-to-string": "^3.1.1", 94 | "rimraf": "^2.3.4", 95 | "touch": "^1.0.0", 96 | "uevents": "^1.0.0", 97 | "webpack": "^1.9.6" 98 | }, 99 | "npmName": "preact-layout", 100 | "npmFileMap": [ 101 | { 102 | "basePath": "/dist/", 103 | "files": [ 104 | "*.js" 105 | ] 106 | } 107 | ], 108 | "browserify": { 109 | "transform": [ 110 | "loose-envify" 111 | ] 112 | }, 113 | "jest": { 114 | "testRegex": "(/test/.*\\.spec.jsx)$", 115 | "moduleFileExtensions": [ 116 | "js", 117 | "jsx" 118 | ] 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /preact-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Download/preact-layout/e4bd240373bcffc7108fc73156f1213629216a0d/preact-layout.png -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact' 2 | 3 | function Layout({className, recurse, children, ...props}, context) { 4 | const { main, sections } = getSections(children) 5 | processNode(main, sections, { ...context }, recurse) 6 | return children && children.length === 1 ? children[0] : ( 7 |
{children}
8 | ) 9 | } 10 | 11 | function Section({type, children, ...props}, context) {return ( 12 | children && (children.length === 1) ? children[0] : ( 13 |
{children}
14 | ) 15 | )} 16 | 17 | function getSections(n, result) { 18 | if (!result) result = {sections:[]} 19 | if (n.nodeName === Section) { 20 | if (n.attributes && n.attributes.type) result.sections.push(n) 21 | else result.main = n 22 | } 23 | const children = Array.isArray(n) ? n : n.children 24 | children && children.forEach(c => { 25 | getSections(c, result) 26 | }) 27 | return result 28 | } 29 | 30 | function processNode(node, sections, context, recurse, collectOnly, results) { 31 | const leftovers = [], postProcess = !results 32 | context = context || {} 33 | if (recurse === undefined) recurse = 9 34 | results = results || {} 35 | sections.forEach(s => results[s.attributes.type] = results[s.attributes.type] || s.children || []) 36 | node && node.children && node.children.forEach(n => { 37 | if (isContribution(n, sections)) { 38 | if (! results[n.nodeName]) results[n.nodeName] = [] 39 | if (n.attributes && n.attributes.append) results[n.nodeName].push.apply(results[n.nodeName], n.children || []) 40 | else if (n.attributes && n.attributes.prepend) results[n.nodeName].unshift.apply(results[n.nodeName], n.children || []) 41 | else results[n.nodeName] = n.children || [] 42 | return // continue 43 | } 44 | leftovers.push(n) 45 | if (typeof n.nodeName == 'function' && recurse) { 46 | let props = { ...n.nodeName.defaultProps, ...n.attributes, children:n.children } 47 | if (n.nodeName.prototype && typeof n.nodeName.prototype.render == 'function') { 48 | let rn, c = new n.nodeName(props, context); 49 | c.props = props; 50 | c.context = context; 51 | if (c.componentWillMount) c.componentWillMount(); 52 | n = c.render(c.props, c.state, c.context); 53 | if (c.getChildContext) context = { ...context, ...c.getChildContext() } 54 | } 55 | else n = n.nodeName(props, context) 56 | recurse-- 57 | } 58 | processNode(n, sections, context, recurse, collectOnly, results) 59 | }) 60 | if (! collectOnly) { 61 | if (node.children) node.children = leftovers 62 | if (postProcess) sections.forEach(s => s.children = results[s.attributes.type]) 63 | } 64 | return results 65 | } 66 | 67 | function isContribution(n, sections) { 68 | return sections.filter(s => n.nodeName === s.attributes.type).length > 0 69 | } 70 | 71 | 72 | export { 73 | Layout, 74 | Section, 75 | 76 | getSections, 77 | isContribution, 78 | processNode 79 | } 80 | -------------------------------------------------------------------------------- /test/Layout.spec.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact' 2 | import render from 'preact-render-to-string' 3 | import EventEmitter from 'uevents' 4 | 5 | import { Layout, Section, getSections, isContribution, processNode } from '../src' 6 | 7 | describe('preact-layout', () => { 8 | it('exports `Layout`, `Section`, `getSections`, `isContribution` and `processNode`', () => { 9 | expect(typeof Layout).toBe('function') 10 | expect(typeof Section).toBe('function') 11 | expect(typeof getSections).toBe('function') 12 | expect(typeof isContribution).toBe('function') 13 | expect(typeof processNode).toBe('function') 14 | }) 15 | 16 | describe('Layout & Section', () => { 17 | it('allows children from the main section to contribute components to other sections of the layout', () => { 18 | function Header(){} 19 | function Footer(){} 20 | 21 | function MyPage(){ 22 | return ( 23 |
24 |

my page

25 |
main article
26 |
goodbye
27 |
28 | ) 29 | } 30 | 31 | let result = render( 32 | 33 |
34 |
35 | header 36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 | footer 44 |
45 |
46 |
47 | ) 48 | 49 | // work around preact-render-to-string generating comment 50 | // nodes for empty (null/undefined) vdom elements. It's 51 | // a bug according to Jason 52 | result = result.replace(//g, '') 53 | 54 | expect(result).toBe('

my page

main article
goodbye
') 55 | }) 56 | 57 | it('allows for the layout to be componentized separately', () => { 58 | function Header(){} 59 | function Footer(){} 60 | 61 | function MyLayout({children, ...props}) { 62 | return ( 63 | 64 |
65 |
66 | header 67 |
68 | 69 |
70 | {children} 71 |
72 | 73 |
74 | footer 75 |
76 |
77 |
78 | ) 79 | } 80 | 81 | const result = render( 82 | 83 |

my page

84 |
main article
85 |
goodbye
86 |
87 | ) 88 | 89 | expect(result).toBe('

my page

main article
goodbye
') 90 | }) 91 | 92 | it('allows contributions from nested children', () => { 93 | function Header(){} 94 | function Footer(){} 95 | 96 | function MyLayout({children, ...props}) {return ( 97 | 98 |
99 |
100 |
header
101 |
102 | 103 |
{children}
104 | 105 |
106 |
footer
107 |
108 |
109 |
110 | )} 111 | 112 | function MyPage(){ 113 | return ( 114 |
115 |

my page

116 |
main article
117 |
goodbye
118 |
119 | ) 120 | } 121 | 122 | let result = render( 123 | 124 | 125 | 126 | ) 127 | 128 | // work around preact-render-to-string generating comment 129 | // nodes for empty (null/undefined) vdom elements. It's 130 | // a bug according to Jason 131 | result = result.replace(//g, '') 132 | 133 | const expectedResult = ` 134 |
135 |
136 |

my page

137 |
138 |
139 |
main article
140 |
141 |
142 | goodbye 143 |
144 |
145 | `.replace(/\s+\s+/g, '>') 146 | 147 | expect(result).toBe(expectedResult) 148 | }) 149 | 150 | it('recurses 9 levels by default', () => { 151 | function Header(){} 152 | function Footer(){} 153 | 154 | function MyLayout({children, ...props}) { 155 | return ( 156 | 157 |
158 |
159 | header 160 |
161 | 162 |
163 | {children} 164 |
165 | 166 |
167 | footer 168 |
169 |
170 |
171 | ) 172 | } 173 | 174 | function MyPage(){ 175 | return ( 176 |
177 |

my page

178 |
main article
179 |
goodbye
180 |
181 | ) 182 | } 183 | 184 | function Level({children}){ 185 | return
{children}
186 | } 187 | 188 | // 2 levels 189 | // nested children above tested 1 level deep, this tests 2 levels deep 190 | let result = render( 191 | 192 | 193 | 194 | 195 | 196 | ) 197 | 198 | expect(result.indexOf('my page')).not.toBe(-1) 199 | 200 | // 3 levels 201 | result = render( 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | ) 210 | 211 | expect(result.indexOf('my page')).not.toBe(-1) 212 | 213 | // 4 levels 214 | result = render( 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | ) 225 | 226 | expect(result.indexOf('my page')).not.toBe(-1) 227 | 228 | // 5 levels 229 | result = render( 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | ) 242 | 243 | expect(result.indexOf('my page')).not.toBe(-1) 244 | 245 | // 6 levels 246 | result = render( 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | ) 261 | 262 | expect(result.indexOf('my page')).not.toBe(-1) 263 | 264 | // 7 levels 265 | result = render( 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | ) 282 | 283 | expect(result.indexOf('my page')).not.toBe(-1) 284 | 285 | // 8 levels 286 | result = render( 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | ) 305 | 306 | expect(result.indexOf('my page')).not.toBe(-1) 307 | 308 | // 9 levels 309 | result = render( 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | ) 330 | 331 | expect(result.indexOf('my page')).not.toBe(-1) 332 | 333 | // 10 levels 334 | result = render( 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | ) 357 | 358 | expect(result.indexOf('my page')).toBe(-1) 359 | }) 360 | 361 | 362 | it('allows the recursion depth to be set on the layout', () => { 363 | function Header(){} 364 | function Footer(){} 365 | 366 | function MyLayout({children, ...props}) { 367 | return ( 368 | 369 |
370 |
371 | header 372 |
373 | 374 |
375 | {children} 376 |
377 | 378 |
379 | footer 380 |
381 |
382 |
383 | ) 384 | } 385 | 386 | function MyPage(){ 387 | return ( 388 |
389 |

my page

390 |
main article
391 |
goodbye
392 |
393 | ) 394 | } 395 | 396 | function Level({children}){ 397 | return
{children}
398 | } 399 | 400 | // 2 levels 401 | // nested children above tested 1 level deep, this tests 2 levels deep 402 | let result = render( 403 | 404 | 405 | 406 | 407 | 408 | ) 409 | 410 | expect(result.indexOf('my page')).not.toBe(-1) 411 | 412 | // 3 levels 413 | result = render( 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | ) 422 | 423 | expect(result.indexOf('my page')).toBe(-1) 424 | }) 425 | 426 | it('allows contribution functions to be used across multiple layouts', () => { 427 | function Header(){} 428 | function Footer(){} 429 | 430 | function MyLayout({children, ...props}) {return ( 431 | 432 |
433 |
434 |
header
435 |
436 |
{children}
437 |
438 |
footer
439 |
440 |
441 |
442 | )} 443 | 444 | function ReversedLayout({children, ...props}) {return ( 445 | 446 |
447 |
448 |
footer
449 |
450 |
{children}
451 |
452 |
header
453 |
454 |
455 |
456 | )} 457 | 458 | function MyPage(){return ( 459 |
460 |

my page

461 |
main article
462 |
goodbye
463 |
464 | )} 465 | 466 | let result = render( 467 | 468 | 469 | 470 | ) 471 | 472 | // work around preact-render-to-string generating comment 473 | // nodes for empty (null/undefined) vdom elements. It's 474 | // a bug according to Jason 475 | result = result.replace(//g, '') 476 | 477 | const expectedResult = ` 478 |
479 |
480 |

my page

481 |
482 |
483 |
main article
484 |
485 |
486 | goodbye 487 |
488 |
489 | `.replace(/\s+\s+/g, '>') 490 | 491 | expect(result).toBe(expectedResult) 492 | 493 | result = render( 494 | 495 | 496 | 497 | ) 498 | 499 | // work around preact-render-to-string generating comment 500 | // nodes for empty (null/undefined) vdom elements. It's 501 | // a bug according to Jason 502 | result = result.replace(//g, '') 503 | 504 | const reversedExpected = ` 505 |
506 |
507 | goodbye 508 |
509 |
510 |
main article
511 |
512 |
513 |

my page

514 |
515 |
516 | `.replace(/\s+\s+/g, '>') 517 | 518 | expect(result).toBe(reversedExpected) 519 | }) 520 | 521 | it('works with component classes as well', () => { 522 | function Header(){} 523 | function Footer(){} 524 | 525 | class MyLayout extends Component { 526 | render({children, ...props}) {return ( 527 | 528 |
529 |
530 |
header
531 |
532 |
{children}
533 |
534 |
footer
535 |
536 |
537 |
538 | )} 539 | } 540 | 541 | class ReversedLayout extends Component { 542 | render({children, ...props}) {return ( 543 | 544 |
545 |
546 |
footer
547 |
548 |
{children}
549 |
550 |
header
551 |
552 |
553 |
554 | )} 555 | } 556 | 557 | class MyPage extends Component { 558 | render(){return ( 559 |
560 |

my page

561 |
main article
562 |
goodbye
563 |
564 | )} 565 | } 566 | 567 | let result = render( 568 | 569 | 570 | 571 | ) 572 | 573 | // work around preact-render-to-string generating comment 574 | // nodes for empty (null/undefined) vdom elements. It's 575 | // a bug according to Jason 576 | result = result.replace(//g, '') 577 | 578 | const expectedResult = ` 579 |
580 |
581 |

my page

582 |
583 |
584 |
main article
585 |
586 |
587 | goodbye 588 |
589 |
590 | `.replace(/\s+\s+/g, '>') 591 | 592 | expect(result).toBe(expectedResult) 593 | 594 | result = render( 595 | 596 | 597 | 598 | ) 599 | 600 | // work around preact-render-to-string generating comment 601 | // nodes for empty (null/undefined) vdom elements. It's 602 | // a bug according to Jason 603 | result = result.replace(//g, '') 604 | 605 | const reversedExpected = ` 606 |
607 |
608 | goodbye 609 |
610 |
611 |
main article
612 |
613 |
614 |

my page

615 |
616 |
617 | `.replace(/\s+\s+/g, '>') 618 | 619 | expect(result).toBe(reversedExpected) 620 | }) 621 | }) 622 | 623 | 624 | describe('getSections(vnode)', () => { 625 | it('accepts a vnode (or array of vnodes) and returns any sections found in it', () => { 626 | function Header(){} 627 | function Footer(){} 628 | 629 | const children = undefined 630 | const markup = ( 631 | 632 |
633 |
634 |
header
635 |
636 |
{children}
637 |
638 |
footer
639 |
640 |
641 |
642 | ) 643 | const result = getSections(markup) 644 | const expected = { 645 | sections:[ 646 |
header
, 647 |
footer
648 | ], 649 | main:
{children}
650 | } 651 | expect(result).toEqual(expected) 652 | }) 653 | 654 | it('does not render any components found (shallow search)', () => { 655 | function Header(){} 656 | function Footer(){} 657 | 658 | function MyLayout({children}) {return ( 659 | 660 |
661 |
662 |
header
663 |
664 |
{children}
665 |
666 |
footer
667 |
668 |
669 |
670 | )} 671 | const markup = 672 | const result = getSections(markup) 673 | const expected = { 674 | sections:[], 675 | main: undefined 676 | } 677 | expect(result).toEqual(expected) 678 | }) 679 | }) 680 | 681 | 682 | describe('isContribution(vnode, sections)', () => { 683 | it('accepts a single vnode and an array of sections', () => { 684 | function Header(){} 685 | const markup =
686 | const { sections } = getSections(markup) 687 | const result = isContribution(
, sections) 688 | const expected = false 689 | expect(result).toBe(expected) 690 | }) 691 | 692 | it('returns true if the vnode is a contribution for any of the given sections', () => { 693 | function Header(){} 694 | function Footer(){} 695 | const children = undefined 696 | const markup = ( 697 | 698 |
699 |
700 |
header
701 |
702 |
{children}
703 |
704 |
footer
705 |
706 |
707 |
708 | ) 709 | const { sections } = getSections(markup) 710 | const result = isContribution(
, sections) 711 | const expected = true 712 | expect(result).toBe(expected) 713 | }) 714 | }) 715 | }) 716 | 717 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack') 4 | 5 | var env = process.env.NODE_ENV 6 | var config = { 7 | resolve: { 8 | // IMPORTANT: Setting this option will override the default, meaning that webpack 9 | // will no longer try to resolve modules using the default extensions. If you want 10 | // modules that were required with their extension (e.g. require('./somefile.ext')) 11 | // to be properly resolved, you must include an empty string in your array. 12 | // Similarly, if you want modules that were required without extensions (e.g. 13 | // require('underscore')) to be resolved to files with “.js” extensions, you must 14 | // include ".js" in your array. 15 | // Default: ["", ".webpack.js", ".web.js", ".js"] 16 | // https://webpack.github.io/docs/configuration.html#resolve-extensions 17 | extensions: ['', '.webpack.js', '.web.js', '.js', '.json', '.jsx'], 18 | }, 19 | module: { 20 | loaders: [ 21 | { test: /\.jsx$/, loaders: ['babel-loader'], exclude: /node_modules/ } 22 | ] 23 | }, 24 | externals: { 25 | preact: 'preact', 26 | }, 27 | output: { 28 | libraryTarget: 'umd' 29 | }, 30 | plugins: [ 31 | new webpack.optimize.OccurrenceOrderPlugin(), 32 | new webpack.DefinePlugin({ 33 | 'process.env.NODE_ENV': JSON.stringify(env) 34 | }) 35 | ] 36 | }; 37 | 38 | if (env === 'production') { 39 | config.plugins.push( 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compressor: { 42 | pure_getters: true, 43 | unsafe: true, 44 | unsafe_comps: true, 45 | warnings: false 46 | } 47 | }) 48 | ) 49 | } 50 | 51 | module.exports = config 52 | --------------------------------------------------------------------------------