├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── package.json
├── rollup.config.js
├── src
├── contextualize.js
├── create-cycle.js
└── index.js
└── test
└── index.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,.*rc,*.yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.md
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "eslint:recommended",
4 | "env": {
5 | "browser": true
6 | },
7 | "plugins": [
8 | "react"
9 | ],
10 | "settings": {
11 | "react": {
12 | "pragma": "h"
13 | }
14 | },
15 | "parserOptions": {
16 | "ecmaVersion": 6,
17 | "sourceType": "module",
18 | "ecmaFeatures": {
19 | "modules": true,
20 | "jsx": true
21 | }
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 | "semi": 2,
29 | "camelcase": 0,
30 | "comma-style": 2,
31 | "comma-dangle": [2, "never"],
32 | "indent": [2, "tab", {"SwitchCase": 1}],
33 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
34 | "no-trailing-spaces": [2, { "skipBlankLines": true }],
35 | "max-nested-callbacks": [2, 3],
36 | "no-eval": 2,
37 | "no-implied-eval": 2,
38 | "no-new-func": 2,
39 | "guard-for-in": 2,
40 | "eqeqeq": 2,
41 | "no-else-return": 2,
42 | "no-redeclare": 2,
43 | "no-dupe-keys": 2,
44 | "radix": 2,
45 | "strict": [2, "never"],
46 | "no-shadow": 0,
47 | "callback-return": [1, ["callback", "cb", "next", "done"]],
48 | "no-delete-var": 2,
49 | "no-undef-init": 2,
50 | "no-shadow-restricted-names": 2,
51 | "handle-callback-err": 0,
52 | "no-lonely-if": 2,
53 | "space-return-throw-case": 2,
54 | "constructor-super": 2,
55 | "no-this-before-super": 2,
56 | "no-dupe-class-members": 2,
57 | "no-const-assign": 2,
58 | "prefer-spread": 2,
59 | "no-useless-concat": 2,
60 | "no-var": 2,
61 | "object-shorthand": 2,
62 | "prefer-arrow-callback": 2
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | /npm-debug.log
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .eslintrc
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 4
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 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-cycle
2 |
3 | [](https://www.npmjs.com/package/preact-cycle)
4 | [](https://travis-ci.org/developit/preact-cycle)
5 |
6 | > Minimal functional _(-reactive)_ Virtual DOM rendering using [Preact].
7 |
8 |
9 | ---
10 |
11 |
12 | ### Simple Example
13 |
14 | [**View this example on esnextb.in**](http://esnextb.in/?gist=d804796c481218488309)
15 |
16 | ```js
17 | import { render, h } from 'preact-cycle';
18 | /** @jsx h */
19 |
20 | const App = ({ value, mutation }) => (
21 |
22 |
Value: { value }
23 |
24 |
25 | );
26 |
27 | render(App, { value: 0 });
28 | ```
29 |
30 |
31 | ---
32 |
33 |
34 | ### To-Do List Example
35 |
36 | A simple example, where reducers are just pure functions.
37 | Note that `TOGGLE` mutates state in-place, which works fine but is discouraged.
38 |
39 | [**View this example on CodePen**](https://codepen.io/developit/pen/XYvQjr?editors=0110)
40 |
41 | ```js
42 | import { render, h } from 'preact-cycle';
43 | /** @jsx h */
44 |
45 | const ADD = ({ text, todos, ...state }) => ({
46 | todos: todos.concat({ text }),
47 | text: '',
48 | ...state
49 | });
50 |
51 | const TOGGLE = (state, todo) => {
52 | todo.done = !todo.done;
53 | return state;
54 | };
55 |
56 | const REMOVE = ({ todos, ...state }, todo) => ({
57 | todos: todos.filter( t => t!==todo ),
58 | ...state
59 | });
60 |
61 |
62 | const TodoList = ({ text, todos, mutate, mutation }) => (
63 |
78 | );
79 |
80 | render(TodoList, { todos: [] }, document.body);
81 | ```
82 |
83 |
84 | ---
85 |
86 |
87 | ### Component-Based Example
88 |
89 | Normal [preact] components still work great with preact-cycle. As of `v0.4`, `mutate()` and `mutation()` are conveniently available as [context] properties, which means they are automatically passed down through the VDOM tree. For pure functional components, [context] is simply passed as a second argument.
90 |
91 | A component-based variant of the previous To-Do List example follows, using pure functions and context.
92 |
93 | ```js
94 | import { h, render } from 'preact-cycle';
95 | /** @jsx h */
96 |
97 |
98 | /** initial data to populate the store */
99 | const INITIAL_DATA = {
100 | todos: [
101 | { text:'Type some text' },
102 | { text:'...then hit [enter]' },
103 | { text:'Now you\'re productive!' }
104 | ]
105 | };
106 |
107 | /** Appends a new todo item */
108 | const ADD = ({ todos, text, ...state }) => ({
109 | todos: todos.concat({ text }),
110 | text: '',
111 | ...state
112 | });
113 |
114 | /** Remove the given todo item */
115 | const REMOVE = ({ todos, ...state }, todo) => ({
116 | todos: todos.filter(t => t!==todo),
117 | ...state
118 | });
119 |
120 | /** Toggles the given todo item as done */
121 | const TOGGLE = (state, todo) => {
122 | todo.done = !todo.done;
123 | };
124 |
125 |
126 | /** a simple helper to derive a mutated value from an event */
127 | let fromEvent = (prev, e) => e.target.value;
128 |
129 |
130 | /** The todo list app */
131 | const App = ({ text, todos }) => (
132 |
133 |
134 |
{ todos.map( todo => (
135 |
136 | )) }
137 |
138 | );
139 |
140 | /** New todo entry form */
141 | const Form = ({ text }, { mutation }) => (
142 |
147 | );
148 |
149 | /** A single todo list item */
150 | const Item = ({ todo }, { mutation }) => (
151 |
152 |
153 | ✕
154 | { todo.text }
155 |
156 | );
157 |
158 | // Kick off the cycle!
159 | render(App, INITIAL_DATA, document.body);
160 | ```
161 |
162 |
163 | ---
164 |
165 |
166 | ### License
167 |
168 | [MIT]
169 |
170 |
171 | [Preact]: https://github.com/developit/preact
172 | [context]: https://facebook.github.io/react/docs/context.html
173 | [MIT]: http://choosealicense.com/licenses/mit/
174 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "preact-cycle",
3 | "amdName": "preactCycle",
4 | "version": "0.5.1",
5 | "description": "Minimal functional Virtual DOM rendering using Preact.",
6 | "main": "dist/preact-cycle.js",
7 | "minified:main": "dist/preact-cycle.min.js",
8 | "jsnext:main": "src/index.js",
9 | "scripts": {
10 | "build": "npm-run-all transpile optimize minify",
11 | "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",
12 | "optimize": "uglifyjs $npm_package_main -bc -o $npm_package_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_main}.map",
13 | "minify": "uglifyjs $npm_package_main -cm -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map",
14 | "test": "eslint {src,test} && mocha --compilers js:babel-register test/**/*.js",
15 | "prepublish": "npm run build",
16 | "release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish"
17 | },
18 | "babel": {
19 | "presets": [
20 | "es2015-minimal",
21 | "stage-0"
22 | ],
23 | "plugins": [
24 | [
25 | "transform-react-jsx",
26 | {
27 | "pragma": "h"
28 | }
29 | ]
30 | ]
31 | },
32 | "keywords": [
33 | "preact",
34 | "frp",
35 | "cycle"
36 | ],
37 | "author": "Jason Miller ",
38 | "license": "MIT",
39 | "repository": {
40 | "type": "git",
41 | "url": "https://github.com/developit/preact-cycle.git"
42 | },
43 | "bugs": {
44 | "url": "https://github.com/developit/preact-cycle/issues"
45 | },
46 | "homepage": "https://github.com/developit/preact-cycle",
47 | "devDependencies": {
48 | "babel": "^6.5.1",
49 | "babel-eslint": "^4.1.8",
50 | "babel-plugin-transform-react-jsx": "^6.6.5",
51 | "babel-preset-es2015": "^6.5.0",
52 | "babel-preset-es2015-minimal": "^1.1.0",
53 | "babel-preset-es2015-minimal-rollup": "^1.1.0",
54 | "babel-preset-stage-0": "^6.5.0",
55 | "babel-register": "^6.5.1",
56 | "chai": "^3.5.0",
57 | "eslint": "^1.10.3",
58 | "eslint-plugin-react": "^4.2.1",
59 | "mocha": "^2.4.5",
60 | "npm-run-all": "^1.5.2",
61 | "rollup": "^0.25.3",
62 | "rollup-plugin-babel": "^2.3.9",
63 | "uglify-js": "^2.6.1"
64 | },
65 | "dependencies": {
66 | "preact": "^7.1.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 |
3 | export default {
4 | plugins: [
5 | babel({
6 | babelrc: false,
7 | sourceMap: true,
8 | exclude: 'node_modules/**',
9 | presets: [
10 | ['es2015', { loose:true, modules:false }],
11 | 'stage-0'
12 | ],
13 | plugins: [
14 | ['transform-react-jsx', { pragma:'h' }]
15 | ]
16 | })
17 | ]
18 | };
19 |
--------------------------------------------------------------------------------
/src/contextualize.js:
--------------------------------------------------------------------------------
1 | /** High-order component that passes its props down as context. */
2 | export default function Contextualize() {}
3 |
4 | Contextualize.prototype.getChildContext = function() {
5 | let { children, ...context } = this.props;
6 | return context;
7 | };
8 |
9 | Contextualize.prototype.render = ({ children }) => children[0];
10 |
--------------------------------------------------------------------------------
/src/create-cycle.js:
--------------------------------------------------------------------------------
1 | /** A little helper library to do FR.
2 | * @param {Function} renderer A function to perform a single render
3 | * @param {Object} [data={}] Initial data
4 | */
5 | export default function createCycle(renderer, data={}) {
6 | let debounce;
7 |
8 | function render() {
9 | clearTimeout(debounce);
10 | debounce = null;
11 | return renderer({ mutate, mutation, ...data });
12 | }
13 |
14 | // optionally key-specific mutation of data
15 | // eg: mutate('value', v => v*2 )
16 | function mutate(fn, ...args) {
17 | let key, r;
18 | if (typeof fn==='string') {
19 | key = fn;
20 | fn = args.splice(0, 1)[0];
21 | }
22 | let p = key ? data[key] : data;
23 | if (typeof fn!=='function') p = fn;
24 | else if ( (r=fn(p, ...args))!==undefined ) p = r;
25 | if (key) data[key] = p;
26 | else data = p;
27 | if (!debounce) debounce = setTimeout(render, 1);
28 | }
29 |
30 | // mutation future/thunk
31 | // eg: let double = mutation('value', v => v*2 )
32 | let mutation = (...args) => (...args2) => mutate(...args, ...args2);
33 |
34 | return render();
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { h, render as doRender } from 'preact';
2 | import Contextualize from './contextualize';
3 | import createCycle from './create-cycle';
4 |
5 | function createRenderer(Renderable, parent) {
6 | let root;
7 | return props => {
8 | root = doRender((
9 |
10 |
11 |
12 | ), parent, root);
13 | };
14 | }
15 |
16 | export default function render(Renderable, data, parent) {
17 | let renderer = createRenderer(Renderable, parent);
18 | return createCycle(renderer, data);
19 | }
20 |
21 | render.render = render;
22 | render.h = h;
23 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import preactCycle from '../src';
3 | /**@jsx h */
4 |
5 | /*eslint-env mocha*/
6 |
7 | describe('preact-cycle', () => {
8 | expect(preactCycle.h).to.be.a('function');
9 | expect(preactCycle.render).to.be.a('function');
10 |
11 | xit('should have proper tests', () => {});
12 | });
13 |
--------------------------------------------------------------------------------