├── .gitignore ├── collaborators.md ├── example.js ├── index.js ├── package.json ├── readme.md ├── test.js ├── update-events.js └── yoyojs.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /collaborators.md: -------------------------------------------------------------------------------- 1 | ## Collaborators 2 | 3 | yo-yo is only possible due to the excellent work of the following collaborators: 4 | 5 | 6 | 7 | 8 | 9 |
maxogdenGitHub/maxogden
shamaGitHub/shama
yoshuawuytsGitHub/yoshuawuyts
freeman-labGitHub/freeman-lab
10 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var yo = require('yo-yo') 2 | 3 | var numbers = [] // start empty 4 | var el = list(numbers, update) 5 | 6 | function list (items, onclick) { 7 | return yo`
Random Numbers 8 | 13 | 14 |
` 15 | } 16 | 17 | function update () { 18 | // add a new random number to our list 19 | numbers.push(Math.random()) 20 | 21 | // construct a new list and efficiently diff+morph it into the one in the DOM 22 | var newList = list(numbers, update) 23 | yo.update(el, newList) 24 | } 25 | 26 | document.body.appendChild(el) 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var bel = require('bel') // turns template tag into DOM elements 2 | var morphdom = require('morphdom') // efficiently diffs + morphs two DOM elements 3 | var defaultEvents = require('./update-events.js') // default events to be copied when dom elements update 4 | 5 | module.exports = bel 6 | 7 | // TODO move this + defaultEvents to a new module once we receive more feedback 8 | module.exports.update = function (fromNode, toNode, opts) { 9 | if (!opts) opts = {} 10 | if (opts.events !== false) { 11 | if (!opts.onBeforeElUpdated) opts.onBeforeElUpdated = copier 12 | } 13 | 14 | return morphdom(fromNode, toNode, opts) 15 | 16 | // morphdom only copies attributes. we decided we also wanted to copy events 17 | // that can be set via attributes 18 | function copier (f, t) { 19 | // copy events: 20 | var events = opts.events || defaultEvents 21 | for (var i = 0; i < events.length; i++) { 22 | var ev = events[i] 23 | if (t[ev]) { // if new element has a whitelisted attribute 24 | f[ev] = t[ev] // update existing element 25 | } else if (f[ev]) { // if existing element has it and new one doesnt 26 | f[ev] = undefined // remove it from existing element 27 | } 28 | } 29 | var oldValue = f.value 30 | var newValue = t.value 31 | // copy values for form elements 32 | if ((f.nodeName === 'INPUT' && f.type !== 'file') || f.nodeName === 'SELECT') { 33 | if (!newValue && !t.hasAttribute('value')) { 34 | t.value = f.value 35 | } else if (newValue !== oldValue) { 36 | f.value = newValue 37 | } 38 | } else if (f.nodeName === 'TEXTAREA') { 39 | if (t.getAttribute('value') === null) f.value = t.value 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yo-yo", 3 | "version": "1.4.1", 4 | "description": "A tiny library for building modular UI components using DOM diffing and ES6 tagged template literals", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "wzrd test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/maxogden/yo-yo.git" 12 | }, 13 | "keywords": [], 14 | "author": "Max Ogden", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/maxogden/yo-yo/issues" 18 | }, 19 | "homepage": "https://github.com/maxogden/yo-yo#readme", 20 | "dependencies": { 21 | "bel": "^4.0.0", 22 | "morphdom": "^2.1.0" 23 | }, 24 | "devDependencies": { 25 | "browserify": "^13.0.1", 26 | "tape": "^4.5.1", 27 | "wzrd": "^1.3.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # yo-yo.js 2 | 3 | A tiny library for building modular UI components using [DOM diffing](#morphdom) and [ES6 tagged template literals](#tagged-template-literals), powered by [bel](https://www.npmjs.com/package/bel) and [morphdom](https://www.npmjs.com/package/morphdom) and based on the "yo-yo" data binding pattern: data down, actions up. 4 | 5 | yo-yo powers the [choo framework](https://github.com/yoshuawuyts/choo), you should check it out if you want something higher level! or if you want lower level, see the module that powers yo-yo: [bel](https://www.npmjs.com/package/bel) 6 | 7 | ![logo](yoyojs.png) 8 | 9 | Getting started is as easy as
var element = yo\`<h1>hello world!</h1>\`
10 | 11 | [Give yo-yo a spin in your browser on RequireBin](http://requirebin.com/?gist=8371be058c7c0c087ebe). 12 | 13 | ## Features 14 | 15 | - React-style modular UI components that can efficiently update themselves 16 | - Build your own framework: [small modules that you can swap out](#modules-that-work-well-with-yo-yo) to pick your own tradeoffs 17 | - Uses features available in browsers today instead of inventing new syntax/APIs 18 | - Designed for [template literals](#tagged-template-literals), a templating feature built in to JS 19 | - Uses a [default DOM diffing](#morphdom) strategy based on the real DOM, not a virtual DOM 20 | - Compatible with vanilla DOM elements and vanilla JS data structures 21 | - Doesn't require hundreds of megabytes of devDependencies to build 22 | - 4kb minified + gzipped (6 times smaller than React), small enough for UI components to include as a dependency 23 | 24 | ## About 25 | 26 | `yo-yo` is a modular UI framework, meaning there isn't much code in this repository, much of the functionality comes from other modules (see [`index.js`](index.js)). The goals of `yo-yo` are to choose a good set of default dependencies, document how to use them all together in one place, and use small enough dependencies that you can include a copy of `yo-yo` in standalone UI component modules and publish them to npm. 27 | 28 | You can start by simply doing `require('yo-yo')` but as your app grows will most likely want to choose different tradeoffs ([add or remove dependencies](#modules-that-work-well-with-yo-yo)), and `yo-yo` is designed to let you do that without rewriting all of your code due to API changes, forcing you to use certain dependencies, or making you adopt new coding conventions. 29 | 30 | In this way `yo-yo` is similar to the modular frameworks [mississippi](https://www.npmjs.com/package/mississippi), [http-framework](https://www.npmjs.com/package/http-framework) and [mercury](https://www.npmjs.com/package/mercury). 31 | 32 | ## Installing 33 | 34 | You can get it [from npm](http://npmjs.org/yo-yo): `npm install yo-yo` 35 | 36 | To create a standalone copy run `browserify --standalone yo index.js > yo-yo.js` 37 | 38 | ## API 39 | 40 | The `yo-yo` API is very simple and only has two functions. 41 | 42 | ### var yo = require('yo-yo') 43 | 44 | Returns the `yo` function. There is also a method on `yo` called `yo.update`. 45 | 46 | ### yo\`template\` 47 | 48 | `yo` is a function designed to be used with [tagged template literals](#tagged-template-literals). If your template produces a string containing an HTML element, the `yo` function will take it and produce a new DOM element that you can insert into the DOM. 49 | 50 | ### yo.update(targetElement, newElement, [opts]) 51 | 52 | Efficiently updates the attributes and content of an element by [diffing and morphing](#morphdom) a new element onto an existing target element. The two elements + their children should have the same 'shape', as the diff between `newElement` will replace nodes in `targetElement`. `targetElement` will get efficiently updated with only the new DOM nodes from `newElement`, and `newElement` can be discarded afterwards. 53 | 54 | Note that many properties of a DOM element **are ignored** when elements are updated. [morphdom](#morphdom) only copies the following properties: 55 | 56 | - `node.firstChild` 57 | - `node.tagName` 58 | - `node.nextSibling` 59 | - `node.attributes` 60 | - `node.nodeType` 61 | - `node.nodeValue` 62 | 63 | In addition to these `yo-yo` will copy event attributes (e.g. `onclick`, `onmousedown`) that you set using DOM attributes in your template. 64 | 65 | `opts` is optional and has these options: 66 | 67 | - `events` - set `false` to disable copying of event attributes. otherwise set to an array of strings, one for each event name you want to whitelist for copying. defaults to our default events 68 | 69 | The `opts` object will also get passed to `morphdom`. 70 | 71 | ## Examples 72 | 73 | Here are some UI modules implemented using `yo-yo`: 74 | 75 | - https://github.com/shama/csv-viewer 76 | - https://github.com/shama/fs-explorer 77 | 78 | And here are some simpler examples: 79 | 80 | ### Creating a simple list 81 | 82 | ```js 83 | var yo = require('yo-yo') 84 | 85 | var el = list([ 86 | 'grizzly', 87 | 'polar', 88 | 'brown' 89 | ]) 90 | 91 | function list (items) { 92 | return yo`` 97 | } 98 | 99 | document.body.appendChild(el) 100 | ```` 101 | 102 | ### Dynamic updates 103 | 104 | ```js 105 | var yo = require('yo-yo') 106 | 107 | var numbers = [] // start empty 108 | var el = list(numbers, update) 109 | 110 | function list (items, onclick) { 111 | return yo`
112 | Random Numbers 113 | 118 | 119 |
` 120 | } 121 | 122 | function update () { 123 | // add a new random number to our list 124 | numbers.push(Math.random()) 125 | 126 | // construct a new list and efficiently diff+morph it into the one in the DOM 127 | var newList = list(numbers, update) 128 | yo.update(el, newList) 129 | } 130 | 131 | document.body.appendChild(el) 132 | ``` 133 | 134 | Clicking the button three times results in this HTML: 135 | 136 | ``` 137 |
Random Numbers 138 | 143 | 144 |
145 | ``` 146 | 147 | When the button is clicked, thanks to `yo.update`, only a single new `
  • ` is inserted into the DOM. 148 | 149 | ### Updating events 150 | 151 | Event handlers starting with `on` that you set via attributes will get updated. 152 | 153 | ```js 154 | function a () { console.log('a') } 155 | function b () { console.log('b') } 156 | 157 | var el = yo`` 158 | el.click() // logs 'a' to console 159 | 160 | var newEl = yo`` 161 | yo.update(el, newEl) 162 | el.click() // logs 'b' to console 163 | ``` 164 | 165 | This works because [we explicitly copy common event attributes](update-events.js). When `yo.update` is called above, `el` is still the same JavaScript Object instance before and after. The only difference is that `yo.update` will copy any new attributes from `newEl` onto `el`. However, if you add custom properties or events to `newEl` before calling `yo.update`, for example `newEl.addEventListener('foo', handleFoo)`, they will not be copied onto `el`. 166 | 167 | ## Modules that work well with yo-yo 168 | 169 | The functionality built in to `yo-yo` covers the same problems as React and JSX, (DOM diffing and templating), using these dependencies of `yo-yo`: 170 | 171 | - [bel](https://npmjs.org/bel) - creates DOM elements from template strings 172 | - [morphdom](https://npmjs.org/morphdom) - efficiently morphs DOM elements (without a virtual DOM) 173 | 174 | However you might consider these alternatives to the above built-in choices based on your use case: 175 | 176 | - [hyperscript](https://npmjs.com/hyperscript) - alternative to template literals 177 | - [diffhtml](https://npmjs.com/diffhtml) - alternative to morphdom 178 | 179 | There are also UI problems that `yo-yo` does not currently address, such as events. But it's easy to use other modules alongside `yo-yo` to create your own framework. We might even add some of these to `yo-yo` in the future: 180 | 181 | ### Older Browser Compatibility / Production Performance 182 | 183 | If you are targeting browsers that may not support template literals and would 184 | like to get a performance boost by transforming your `yo-yo` elements into raw 185 | document calls: 186 | 187 | - [yo-yoify](https://github.com/shama/yo-yoify) 188 | 189 | ### CSS 190 | 191 | - [dom-css](https://npmjs.org/dom-css) - inline CSS helper 192 | - [csjs](https://npmjs.org/csjs) - namespaced CSS helper 193 | - [csjs-extractify](https://github.com/rtsao/csjs-extractify) - csjs browserify transform to compile css bundles 194 | - [csjs-injectify](https://github.com/rtsao/csjs-injectify) - csjs browserify transform that uses [insert-css](https://npmjs.org/insert-css) 195 | - [sheetify](https://github.com/stackcss/sheetify) - browserify modular css transform 196 | - plain css files - you don't always have to use a fancy CSS module :) 197 | 198 | ### State management 199 | 200 | In `yo-yo` state management is left completely up to you. The simplest approach is the "yo-yo" pattern: simply call a callback up until it reaches a parent where you want to handle updates, then `yo.update()` the changes down from there, which keeps the elements isolated. But since you are just working with DOM elements, you can do `yo.update(document.querySelector('.some-other-element'), newelement)` as well. 201 | 202 | There are also some other approaches that introduce their own patterns for managing state: 203 | 204 | - [store-emitter](https://github.com/sethvincent/store-emitter) - redux-inspired state management library 205 | - [minidux](https://github.com/freeman-lab/minidux) - mini version of redux 206 | - [good-old-fashioned-redux-example](https://github.com/freeman-lab/good-old-fashioned-redux-example) - a redux example implemented in un-fancy JS 207 | 208 | ## Overview of default dependencies 209 | 210 | ### bel 211 | 212 | [`bel`](https://npmjs.org/bel) is a module that takes the output from a [tagged template string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) and creates or updates (using DOM diffing) a DOM element tree. 213 | 214 | ### Tagged template literals 215 | 216 | [Tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) are a way to use template literals (AKA template strings) with functions that take the output of the template string and format them in a certain way. 217 | 218 | Regular template literals lets you take code like this: 219 | 220 | ```js 221 | var multiline = 'hello\n' + 222 | 'this\n' + 223 | 'is\n' + 224 | 'multiline' 225 | ``` 226 | 227 | And write the same thing like this instead: 228 | 229 | ```js 230 | var multiline = `hello 231 | this 232 | is 233 | multiline` 234 | ``` 235 | 236 | **Tagged** template literals is where you put a function name in front of the template tags, similar to calling a function with `()` but using the backticks ```` instead of parens. 237 | 238 | ```js 239 | function doesNothing () {} 240 | 241 | doesNothing`im a string` 242 | ``` 243 | 244 | The above example causes the `doesNothing` function to get invoked (AKA called), similar to if you did `doesNothing('im a string')`. 245 | 246 | The difference is that tagged template strings return a specific output value. 247 | 248 | ```js 249 | function logArguments (a, b, c, d) { 250 | console.log(a, b, c, d) 251 | } 252 | 253 | logArguments`im a string` 254 | ``` 255 | 256 | Running the above produces `["im a string", raw: "im a string"] undefined undefined undefined`. 257 | 258 | If you were to just run `console.log(`im a string`)` it would produce `"im a string"`. 259 | 260 | However, tagged template strings return the above tagged template array output format. 261 | 262 | The first item in the array is an array of all of the strings in your template string. In our case there is only one: 263 | 264 | ```js 265 | ["im a string", raw: "im a string"] 266 | ``` 267 | 268 | The `raw` is a property that also contains an array, but where the values are the 'raw' values as there were entered. 269 | 270 | If you had this template for example: 271 | 272 | ```js 273 | logArguments`\u9999` 274 | ``` 275 | 276 | It would produce this as the first argument to logArguments: `["香", raw: ["\u9999"]]` 277 | 278 | In template literals, tagged or not, you can interpolate values by embedding javascript expressions inside of `${}` 279 | 280 | ```js 281 | var name = 'bob' 282 | console.log(`hello ${name}!`) 283 | ``` 284 | 285 | The above produces "hello bob!". However, when called like this: 286 | 287 | ```js 288 | function logArguments (a, b, c, d) { 289 | console.log(a, b, c, d) 290 | } 291 | 292 | var name = 'bob' 293 | logArguments`hello ${name}!` 294 | ``` 295 | 296 | It produces the tagged template array `["hello ", "!", raw: ["hello ", "!"]] "bob" undefined undefined` 297 | 298 | As you can see the first argument is an array of all of the strings, and the rest of the arguments are all of the interpolated values one at a time. 299 | 300 | Using this array you can implement your own custom way to render the strings and values. For example to simply print a string you print the strings and values in 'zipped' order): 301 | 302 | ```js 303 | function printString(strings, valueA, valueB, valueC) { 304 | console.log(strings[0] + valueA + strings[1] + valueB + strings[2] + valueC) 305 | } 306 | ``` 307 | 308 | You could also imagine writing the above function in a more general way using loops etc. Or do something entirely different: 309 | 310 | ### hyperx 311 | 312 | `yo-yo` uses a module called `bel` which in turn uses `hyperx` to turn tagged template arrays into DOM builder data. 313 | 314 | For example: 315 | 316 | ```js 317 | var hyperx = require('hyperx') 318 | 319 | var convertTaggedTemplateOutputToDomBuilder = hyperx(function (tagName, attrs, children) { 320 | console.log(tagName, attrs, children) 321 | }) 322 | 323 | convertTaggedTemplateOutputToDomBuilder`

    hello world

    ` 324 | ``` 325 | 326 | Running this produces `h1 {} [ 'hello world' ]`, which aren't yet DOM elements but have all the data you need to build your own DOM elements however you like. These three arguments, `tagName, attrs, children` are a sort of pseudo-standard used by various DOM building libraries such as [virtual-dom](https://www.npmjs.com/package/virtual-dom), [hyperscript](https://www.npmjs.com/package/hyperscript) and [react](https://facebook.github.io/react/docs/glossary.html#react-elements), and now `hyperx` and `bel`. 327 | 328 | You can also use DOM elements not created using `hyperx` and `bel`: 329 | 330 | ```js 331 | var yo = require('yo-yo') 332 | var vanillaElement = document.createElement('h3') 333 | vanillaElement.textContent = 'Hello' 334 | 335 | var app = yo`
    ${vanillaElement} World
    ` 336 | ``` 337 | 338 | Running the above sets `app` to an element with this HTML: 339 | 340 | ``` 341 |

    Hello

    World
    342 | ``` 343 | 344 | ### morphdom 345 | 346 | `yo-yo` lets you do two basic things: create an element and update it. When you create an element it simply creates a new DOM element tree using hyperx and its own custom code that uses `document.createElement`. 347 | 348 | However, when you update an element using `yo.update()` it actually uses a module called [`morphdom`](https://npmjs.org/morphdom) to transform the existing DOM tree to match the new DOM tree while minimizing the number of changes to the existing DOM tree. This is a really similar approach to what `react` and `virtual-dom` do, except `morphdom` does not use a virtual DOM, it simply uses the actual DOM. 349 | 350 | ## Benchmarks 351 | 352 | You can find benchmarks at https://github.com/shama/yo-yo-perf 353 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /*global Event*/ 2 | var test = require('tape') 3 | var yo = require('./') 4 | 5 | test('event attribute gets updated', function (t) { 6 | t.plan(2) 7 | function a () { t.ok(true, 'called a') } 8 | function b () { t.ok(true, 'called b') } 9 | var el = yo`` 10 | el.click() 11 | yo.update(el, yo``) 12 | el.click() 13 | }) 14 | 15 | test('event attribute gets removed', function (t) { 16 | t.plan(1) 17 | function a () { t.ok(true, 'called a') } 18 | var el = yo`` 19 | el.click() 20 | yo.update(el, yo``) 21 | el.click() 22 | }) 23 | 24 | test('custom event listeners and properties are ignored', function (t) { 25 | t.plan(3) 26 | function a () { t.ok(true, 'called a') } 27 | function b () { t.ok(true, 'called b') } 28 | function c () { t.notOk(true, 'should not call c') } 29 | var el = yo`` 30 | el.click() 31 | var newEl = yo`` 32 | newEl.foo = 999 33 | newEl.addEventListener('foobar', c) 34 | yo.update(el, newEl) 35 | t.equal(el.foo, undefined, 'no el.foo') 36 | el.dispatchEvent(new Event('foobar')) 37 | el.click() 38 | }) 39 | 40 | test('input values get copied', function (t) { 41 | t.plan(1) 42 | var el = yo`` 43 | el.value = 'hi' 44 | var newEl = yo`` 45 | yo.update(el, newEl) 46 | t.equal(el.value, 'hi') 47 | }) 48 | 49 | test('input value gets updated', function (t) { 50 | t.plan(1) 51 | var el = yo`` 52 | el.value = 'howdy' 53 | var newEl = yo`` 54 | newEl.value = 'hi' 55 | yo.update(el, newEl) 56 | t.equal(el.value, 'hi') 57 | }) 58 | 59 | test('input value can be update to empty string', function (t) { 60 | t.plan(1) 61 | var el = yo`` 62 | el.value = 'hola' 63 | var newEl = yo`` 64 | yo.update(el, newEl) 65 | t.equal(el.value, '') 66 | }) 67 | 68 | test('textarea values get copied', function (t) { 69 | t.plan(1) 70 | function textarea (val) { 71 | return yo`` 72 | } 73 | var el = textarea('foo') 74 | yo.update(el, textarea('bar')) 75 | t.equal(el.value, 'bar') 76 | }) 77 | -------------------------------------------------------------------------------- /update-events.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // attribute events (can be set with attributes) 3 | 'onclick', 4 | 'ondblclick', 5 | 'onmousedown', 6 | 'onmouseup', 7 | 'onmouseover', 8 | 'onmousemove', 9 | 'onmouseout', 10 | 'ondragstart', 11 | 'ondrag', 12 | 'ondragenter', 13 | 'ondragleave', 14 | 'ondragover', 15 | 'ondrop', 16 | 'ondragend', 17 | 'onkeydown', 18 | 'onkeypress', 19 | 'onkeyup', 20 | 'onunload', 21 | 'onabort', 22 | 'onerror', 23 | 'onresize', 24 | 'onscroll', 25 | 'onselect', 26 | 'onchange', 27 | 'onsubmit', 28 | 'onreset', 29 | 'onfocus', 30 | 'onblur', 31 | 'oninput', 32 | // other common events 33 | 'oncontextmenu', 34 | 'onfocusin', 35 | 'onfocusout' 36 | ] 37 | -------------------------------------------------------------------------------- /yoyojs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/yo-yo/92e7b0897fb217ce3e456eb3137c4ff2d1c0c801/yoyojs.png --------------------------------------------------------------------------------