├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── karma.conf.js ├── package.json ├── rollup.config.js ├── src └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", {"loose":true}], 4 | "stage-0" 5 | ], 6 | "plugins": [ 7 | ["transform-react-jsx", { "pragma":"h" }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "plugins": [ 5 | "react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "mocha": true, 10 | "es6": true, 11 | "node": true 12 | }, 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "modules": true, 16 | "jsx": true 17 | } 18 | }, 19 | "globals": { 20 | "sinon": true, 21 | "expect": true 22 | }, 23 | "rules": { 24 | "react/jsx-uses-react": 2, 25 | "react/jsx-uses-vars": 2, 26 | "no-unused-vars": [1, { "varsIgnorePattern": "^h$" }], 27 | "no-cond-assign": 1, 28 | "no-empty": 0, 29 | "no-console": 1, 30 | "semi": 2, 31 | "camelcase": 0, 32 | "comma-style": 2, 33 | "comma-dangle": [2, "never"], 34 | "indent": [2, "tab", {"SwitchCase": 1}], 35 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 36 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 37 | "max-nested-callbacks": [2, 5], 38 | "no-eval": 2, 39 | "no-implied-eval": 2, 40 | "no-new-func": 2, 41 | "guard-for-in": 2, 42 | "eqeqeq": 0, 43 | "no-else-return": 2, 44 | "no-redeclare": 2, 45 | "no-dupe-keys": 2, 46 | "radix": 2, 47 | "strict": [2, "never"], 48 | "no-shadow": 0, 49 | "callback-return": [1, ["callback", "cb", "next", "done"]], 50 | "no-delete-var": 2, 51 | "no-undef-init": 2, 52 | "no-shadow-restricted-names": 2, 53 | "handle-callback-err": 0, 54 | "no-lonely-if": 2, 55 | "keyword-spacing": 2, 56 | "constructor-super": 2, 57 | "no-this-before-super": 2, 58 | "no-dupe-class-members": 2, 59 | "no-const-assign": 2, 60 | "prefer-spread": 2, 61 | "no-useless-concat": 2, 62 | "no-var": 2, 63 | "object-shorthand": 2, 64 | "prefer-arrow-callback": 2 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /npm-debug.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jason Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # preact-views 2 | 3 | [![NPM](http://img.shields.io/npm/v/preact-views.svg)](https://www.npmjs.com/package/preact-views) 4 | [![travis-ci](https://travis-ci.org/developit/preact-views.svg)](https://travis-ci.org/developit/preact-views) 5 | 6 | **Named views for [Preact], with easy-as-pie linking between them!** 7 | 8 | `preact-views` provides a `` component that renders its children only when their `name` prop is selected as the current "view". The current view name can be set via a prop, or automatically through the provided `` component. 9 | 10 | > **Note:** `preact-views` is simple and does not do orchestration or routing for you. If you're looking for a URL router, try [preact-router](https://github.com/developit/preact-router). 11 | 12 | #### [See a Real-world Example :arrow_right:](https://jsfiddle.net/developit/jz95kc33/) 13 | 14 | --- 15 | 16 | 17 | ### Complete Example 18 | 19 | ```js 20 | import { Views, Link } from 'preact-views'; 21 | import { h, render } from 'preact'; 22 | 23 | const Home = () => ( 24 |
25 |

Home!

26 | Go Other 27 |
28 | ); 29 | 30 | const Other = ({ value=0 }) => ( 31 |
32 |

Other.

33 | Go Home 34 |

value is {value}.

35 | Increment 36 |
37 | ); 38 | 39 | render(( 40 | 41 | 42 | 43 | 44 | ), document.body); 45 | ``` 46 | 47 | [**See it running :arrow_right:**](https://jsfiddle.net/developit/jz95kc33/) 48 | 49 | 50 | --- 51 | 52 | 53 | ### Simple Example 54 | 55 | ```js 56 | import Views from 'preact-views'; 57 | import { h, render } from 'preact'; 58 | 59 | render(( 60 | 61 |
one
62 |
two
63 |
64 | ), document.body); 65 | 66 | // renders a div containing the text "one" 67 | ``` 68 | 69 | 70 | --- 71 | 72 | 73 | ### License 74 | 75 | [MIT] 76 | 77 | 78 | [Preact]: https://github.com/developit/preact 79 | [MIT]: http://choosealicense.com/licenses/mit/ 80 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['mocha', 'chai-sinon'], 4 | reporters: ['mocha'], 5 | browsers: ['PhantomJS'], 6 | 7 | files: ['test/**/*.js'], 8 | 9 | preprocessors: { 10 | '{src,test}/**/*.js': ['webpack', 'sourcemap'] 11 | }, 12 | 13 | webpack: { 14 | module: { 15 | loaders: [{ 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, 18 | loader: 'babel' 19 | }] 20 | }, 21 | resolve: { 22 | alias: { 23 | src: __dirname+'/src' 24 | } 25 | } 26 | }, 27 | 28 | webpackMiddleware: { 29 | noInfo: true 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-views", 3 | "amdName": "preactViews", 4 | "version": "1.0.0", 5 | "description": "Connect your components up to that address bar.", 6 | "main": "dist/preact-views.js", 7 | "jsnext:main": "src/index.js", 8 | "minified:main": "dist/preact-views.min.js", 9 | "module": "src/index.js", 10 | "scripts": { 11 | "clean": "rimraf dist/", 12 | "build": "npm-run-all clean transpile optimize minify size", 13 | "transpile": "rollup -c rollup.config.js -m ${npm_package_main}.map -f umd -n $npm_package_amdName $npm_package_jsnext_main -o $npm_package_main", 14 | "optimize": "uglifyjs $npm_package_main -bc dead_code --pure-funcs _possibleConstructorReturn _classCallCheck -o $npm_package_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_main}.map", 15 | "minify": "uglifyjs $npm_package_main -mc collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code --pure-funcs _possibleConstructorReturn _classCallCheck -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map", 16 | "size": "echo \"gzip size: $(gzip-size $npm_package_minified_main --raw) bytes\"", 17 | "test": "eslint src test && karma start --single-run", 18 | "prepublish": "npm run -s build && npm test", 19 | "release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" 20 | }, 21 | "files": [ 22 | "src", 23 | "dist" 24 | ], 25 | "keywords": [ 26 | "preact", 27 | "views" 28 | ], 29 | "author": "Jason Miller ", 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/developit/preact-views.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/developit/preact-views/issues" 37 | }, 38 | "homepage": "https://github.com/developit/preact-views", 39 | "peerDependencies": { 40 | "preact": "*" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "^6.9.0", 44 | "babel-core": "^6.9.1", 45 | "babel-eslint": "^7.0.0", 46 | "babel-loader": "^6.2.4", 47 | "babel-plugin-transform-class-properties": "^6.9.1", 48 | "babel-plugin-transform-es2015-classes": "^6.9.0", 49 | "babel-plugin-transform-object-assign": "^6.0.0", 50 | "babel-plugin-transform-react-jsx": "^6.8.0", 51 | "babel-preset-es2015": "^6.9.0", 52 | "babel-preset-react": "^6.5.0", 53 | "babel-preset-stage-0": "^6.5.0", 54 | "chai": "^3.5.0", 55 | "copyfiles": "^1.0.0", 56 | "diff": "^3.0.0", 57 | "eslint": "^3.0.0", 58 | "eslint-plugin-react": "^6.0.0", 59 | "gzip-size-cli": "^2.0.0", 60 | "karma": "^1.0.0", 61 | "karma-chai-sinon": "^0.1.5", 62 | "karma-mocha": "^1.0.1", 63 | "karma-mocha-reporter": "^2.0.3", 64 | "karma-phantomjs-launcher": "^1.0.0", 65 | "karma-sourcemap-loader": "^0.3.7", 66 | "karma-webpack": "^2.0.1", 67 | "mkdirp": "^0.5.1", 68 | "mocha": "^3.0.0", 69 | "npm-run-all": "^4.0.1", 70 | "phantomjs-prebuilt": "^2.1.7", 71 | "preact": "^7.2.0", 72 | "pretty-bytes-cli": "^1.0.0", 73 | "rimraf": "^2.5.1", 74 | "rollup": "^0.41.4", 75 | "rollup-plugin-babel": "^2.4.0", 76 | "rollup-plugin-es3": "^1.0.3", 77 | "rollup-plugin-memory": "^2.0.0", 78 | "rollup-plugin-post-replace": "^1.0.0", 79 | "sinon": "^1.17.4", 80 | "sinon-chai": "^2.8.0", 81 | "uglify-js": "^2.6.1", 82 | "webpack": "^1.13.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import es3 from 'rollup-plugin-es3'; 3 | import replace from 'rollup-plugin-post-replace'; 4 | import babel from 'rollup-plugin-babel'; 5 | import memory from 'rollup-plugin-memory'; 6 | 7 | var babelRc = JSON.parse(fs.readFileSync('.babelrc','utf8')); // eslint-disable-line 8 | 9 | export default { 10 | exports: 'default', 11 | useStrict: false, 12 | globals: { 13 | preact: 'preact' 14 | }, 15 | plugins: [ 16 | memory({ 17 | path: 'src/index', 18 | contents: "export { default } from './index';" 19 | }), 20 | 21 | babel({ 22 | babelrc: false, 23 | presets: [ 24 | ['es2015', { loose:true, modules:false }] 25 | ].concat(babelRc.presets.slice(1)), 26 | plugins: babelRc.plugins, 27 | exclude: 'node_modules/**' 28 | }), 29 | 30 | // strip Object.freeze() 31 | es3(), 32 | 33 | // remove Babel helpers 34 | replace({ 35 | 'throw ': 'return; throw ' 36 | }) 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { h, cloneElement, Component } from 'preact'; 2 | 3 | /** @example 4 | * 5 | * 6 | * 7 | * 8 | */ 9 | export class Views extends Component { 10 | state = { 11 | // can pass default view in as a prop 12 | view: this.props.view, 13 | params: {} 14 | }; 15 | 16 | // show a given view, optionally with params (props) 17 | route = (view, params) => { 18 | params = params || {}; 19 | this.setState({ view, params }); 20 | }; 21 | 22 | // if re-rendered with a new "view" prop, set as active view 23 | componentWillReceiveProps({ view }) { 24 | if (view!==this.props.view) this.route(view); 25 | } 26 | 27 | // Expose route() into context so child components can invoke it: 28 | // this.context.route('some-view'); 29 | getChildContext() { 30 | return { route: this.route }; 31 | } 32 | 33 | // Render the child whose `name` prop matches the current view 34 | render({ children }, { view, params }) { 35 | let child = children.filter( child => child.attributes.name===view )[0]; 36 | return child ? cloneElement(child, params) : null; 37 | } 38 | } 39 | 40 | 41 | /** Renders an `` that, when clicked, directs to the view indicated by a `to` prop. 42 | * @param {Object} props 43 | * @param {String} props.to The name of a view to switch to 44 | * @param {Object} [props.params={}] Props to pass to the view 45 | * @example 46 | * Home 47 | * Foo 48 | */ 49 | export function Link({ to, params, ...props }, context) { 50 | if (!props.onClick) props.onClick = () => context.route(to, params); 51 | return h('a', props); 52 | } 53 | 54 | 55 | // commonjs is the real king 56 | Views.Views = Views; 57 | Views.Link = Link; 58 | export default Views; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { Views, Link } from 'src'; 2 | import { h, render, options } from 'preact'; 3 | 4 | // sync is easier for simple tests 5 | options.debounceRendering = f => f(); 6 | 7 | describe('preact-views', () => { 8 | let scratch = document.createElement('div'), 9 | $ = s => scratch.querySelector(s), 10 | mount = jsx => root = render(jsx, scratch, root), 11 | root; 12 | 13 | afterEach( () => { 14 | mount(); 15 | scratch.innerHTML = ''; 16 | }); 17 | 18 | describe('', () => { 19 | it('should render initial view', () => { 20 | mount( 21 | 22 |
23 |
24 | 25 | ); 26 | 27 | expect(scratch.innerHTML).to.equal('
'); 28 | }); 29 | 30 | describe('switching', () => { 31 | const Home = () => ( 32 |
33 |

Home!

34 | Go Other 35 |
36 | ); 37 | 38 | const Other = ({ value=0 }) => ( 39 |
40 |

Other.

41 | Go Home 42 |

value is {value}.

43 | Increment 44 |
45 | ); 46 | 47 | it('should render when view prop changes', () => { 48 | mount( 49 | 50 | 51 | 52 | 53 | ); 54 | 55 | expect(scratch.innerHTML).to.equal(`
`); 56 | 57 | mount( 58 | 59 | 60 | 61 | 62 | ); 63 | 64 | expect(scratch.innerHTML).to.equal(`

Other.

Go Home

value is 0.

Increment
`); 65 | }); 66 | 67 | it('should render with props when clicking s', () => { 68 | mount( 69 | 70 | 71 | 72 | 73 | ); 74 | 75 | $('a:last-child').click(); 76 | $('a:last-child').click(); 77 | $('a:last-child').click(); 78 | 79 | expect(scratch.innerHTML).to.equal(`

Other.

Go Home

value is 3.

Increment
`); 80 | 81 | $('a').click(); 82 | 83 | expect(scratch.innerHTML).to.equal(``); 84 | }); 85 | }); 86 | }); 87 | }); 88 | --------------------------------------------------------------------------------