├── .babelrc ├── .gitignore ├── README.md ├── dist └── real-dom.js ├── package.json └── src └── real-dom.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babili"], 3 | "plugins": [ 4 | "transform-es2015-arrow-functions", 5 | "transform-es2015-block-scoping", 6 | "transform-es2015-parameters", 7 | ["transform-es2015-modules-umd", { 8 | "globals": { 9 | "real-dom": "realDOM" 10 | }, 11 | "exactGlobals": true 12 | }] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # npm log file 5 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## real-dom: a ~1K non-virtual DOM framework for simple apps 2 | 3 | Virtual DOM frameworks are all the rage. But the real DOM is faster than you might think. Each time you call its dispatch function, Real-DOM simply replaces the entire DOM tree under its root node with your new view. This makes it simple and uncluttered. It's less than 50 lines of code before minifying, and roughly 1K after. It has built-in support for Redux-style actions and reducers, but also can be used more simply, by passing new state directly to the `dispatch` function it passes to the view. The version in `src` is in ES2015 module format and the version in `dist` is in UMD module format. 4 | 5 | #### Without using virtual DOM, won't my app be slow? 6 | Apps with thousands of DOM nodes or that frequently redraw their views may benefit significantly from avoiding DOM mutation. But most applications aren't that complex and do not update very frequently. If you are developing a game or the next Facebook, then you should probably consider using React or one of the many virtual DOM alternatives. If you just want typeahead results or simple todos, you won't really notice it. 7 | 8 | #### If Real-DOM replaces the DOM tree on each update, how does it preserve focus? 9 | Before replacing the view, Real-DOM stores a reference to the document's active element. After replacing the view, Real-DOM will return focus to the element with the same `id` attribute. It also preserves caret position, selection, and scroll position for any element with an `id` attribute. In modern browsers, this happens fast enough that it does not interrupt typing in a non-debounced [controlled input](https://facebook.github.io/react/docs/forms.html#controlled-components). 10 | 11 | #### How does it perform in benchmarks? 12 | Probably not very well. If you have a very complex DOM tree or redraw your view very frequently, it's not the right approach for you. 13 | 14 | #### Is there a plunker I can start from? 15 | Yep. Here's the [starter kit](https://plnkr.co/edit/yZv9QCX5GDkhb1fMDbQC). Real-DOM registers itself on the global variable `realDOM`. 16 | 17 | #### Is this compatiable with JSX? 18 | Yes, if you [configure Babel](https://babeljs.io/docs/plugins/transform-react-jsx/) to use its `h` function instead of `React.createElement`. Keep in mind that Real-DOM does not have a simulated event system like React, and it doesn't accept other full React-style components as arguments. It does, though, accept functions (see the API documentation below). Due to how JSX is translated into function calls, the name of the function has to be capitalized. Here's an [example](https://plnkr.co/edit/CEktyrUwy86s3HTEJJHU). 19 | 20 | #### Can this be rendered on the server side? 21 | Theoretically, but it has not been thoroughly tested. You'll need a synthetic document object, such as [jsdom](https://github.com/tmpvar/jsdom). 22 | 23 | ## API documentation 24 | Real-DOM exposes only two functions: `component` and `h`. `component` takes, at minimum, an initial state and a function that renders your view. It can also take a Redux-style reducer and a factory function for registering callbacks to external events, such as a Web Socket. `h` is named in honor of hyperscript and follows the same syntax with some simplifications. 25 | 26 | ### `component(initialState, view, [reducer, registerSubscriptions])` 27 | 28 | #### Return value 29 | A function `HTMLElement -> void`. When invoked, the function mounts the application as a child of that root node. The root node should not have any other children: they will be removed from the tree on each update of the view. 30 | 31 | #### Arguments 32 | 1. `initialState`: the application's initial state. It accepts any value: object, array, primitive. 33 | 2. `view`: a function `(state, dispatch) -> HTMLElement`. This function will be invoked with `initialState` when the application is mounted, and then again each time the dispatch function is called. The result will be appended as a child to the root node (see the return value above). The `h` function below is provided to make composing this function easier. 34 | 3. `reducer`: a function `(state, action) -> *`. The value returned from the function becomes the application's new state. It is good practice for this to be a pure function. If the action it receives is a `Promise`, it will execute after the `Promise` resolves. The default is `(state, action) => action`. That is, it sets the application's new state to whatever value is passed to the `dispatch` function of the view. 35 | 4. `registerSubscriptions`: a function `dispatch -> void`. The function will be invoked immediately after the application is mounted. Use this function to register callbacks to external services, so that they can call the dispatch function, e.g., a Web Socket, firebase, meteor. 36 | 37 | ### `h(tag|function, [attrs, ...children])` 38 | #### Return value 39 | An `HTMLElement`. 40 | 41 | #### `tag|function` 42 | Either the tag name of the html element or a function. If it is invoked with a function, that function will be called with the value of the `attrs` argument, similar to a stateless functional component in React. 43 | 44 | #### `attrs` 45 | An object containing the attributes to be applied to the html element. Use the JavaScript names for accessing attributes; for example, `className` instead of `class`. It supports registering events by passing functions for `onevent` attributes. The only special handling is for the style attribute, which can take either a string or an object with individual CSS properties. It gracefully handles `null` or `undefined`. 46 | 47 | If a function was passed to `tag`, then that function will be executed with this parameter, without any parsing. (That includes `null` and `undefined`, which will be passed directly.) 48 | 49 | #### `...children` 50 | Any remaining parameters are handled as children to the node. Strings are wrapped with a call to `document.createTextNode`. Arrays are flattened and their elements handled as other parameters. 51 | -------------------------------------------------------------------------------- /dist/real-dom.js: -------------------------------------------------------------------------------- 1 | (function(global,factory){if(typeof define==='function'&&define.amd){define(['exports'],factory)}else if(typeof exports!=='undefined'){factory(exports)}else{var mod={exports:{}};factory(mod.exports);global.realDOM=mod.exports}})(this,function(exports){'use strict';Object.defineProperty(exports,'__esModule',{value:true});var h=exports.h=function(a,b){for(var _len=arguments.length,c=Array(2<_len?_len-2:0),_key=2;_key<_len;_key++)c[_key-2]=arguments[_key];if('function'==typeof a)return a(b);var d=Object.assign(document.createElement(a),b);return Object.assign(d.style,(b||{}).style),c.reduce(function(f,g){return f.concat(g)},[]).forEach(function(f){return d.appendChild('object'==typeof f?f:document.createTextNode(f))}),d};var component=exports.component=function(a,b){var c=2 { 2 | if (typeof tagName === 'function') return tagName(props); 3 | 4 | const node = Object.assign(document.createElement(tagName), props); 5 | Object.assign(node.style, (props || {}).style); 6 | children 7 | .reduce((acc, cur) => acc.concat(cur), []) 8 | .forEach(child => node.appendChild((typeof child === 'object') ? child : document.createTextNode(child))); 9 | 10 | return node; 11 | }; 12 | 13 | export const component = (initState, view, reducer = (state, action) => action, registerSubscriptions = dispatch => null) => root => { 14 | let state = initState; 15 | const cacheProps = e => ({id:e.id, selectionStart:e.selectionStart, selectionEnd:e.selectionEnd, selectionDirection:e.selectionDirection, scrollTop:e.scrollTop, scrollLeft:e.scrollLeft}); 16 | const renderer = tree => { 17 | const focusedId = (document.activeElement || {id:''}).id; 18 | const identifiedElements = Array.prototype.map.call(document.querySelectorAll('[id]'), cacheProps); 19 | while (root.firstChild) root.removeChild(root.firstChild); 20 | root.appendChild(tree); 21 | identifiedElements.forEach(element => { 22 | const newElement = document.getElementById(element.id); 23 | if (newElement) { 24 | if(element.id === focusedId) newElement.focus(); 25 | Object.assign(newElement, element); 26 | } 27 | }); 28 | }; 29 | const dispatch = action => Promise.resolve(action).then(action => { 30 | state = reducer(state, action); 31 | renderer(view(state, dispatch)); 32 | }); 33 | registerSubscriptions(dispatch); 34 | renderer(view(state, dispatch)); 35 | }; 36 | --------------------------------------------------------------------------------