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 |
9 | ${items.map(function (item) {
10 | return yo`
${item}
`
11 | })}
12 |
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 | 
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`
`
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 |
139 |
0.027827488956972957
140 |
0.742044786689803
141 |
0.4440679911058396
142 |
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
--------------------------------------------------------------------------------