├── .editorconfig
├── LICENSE
├── README.md
├── build
├── bundle.js
├── index.css
└── index.html
├── deploy-gh-pages.sh
├── package.json
├── src
├── dispatcher.js
├── index.js
├── tags.js
└── todo-store.js
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig file (see http://EditorConfig.org)
2 |
3 | root = true
4 |
5 | # All files.
6 | [*]
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | indent_style = space
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Stuart Rackham
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 | # Flux-like Riot Todo Application
2 |
3 | This minimalist didactic application is written using the React-like [Riot](https://muut.com/riotjs/) UI library:
4 |
5 | - Written in ES6.
6 | - Compiled using [6to5](http://6to5.org/).
7 | - Bundled with [Webpack](http://webpack.github.io/).
8 | - Uses browser LocalStorage for persistence.
9 |
10 | The app is a port of my [Flux Backbone Todos Example](https://github.com/srackham/flux-backbone-todo) and I wrote it to learn and evaluate Riot.
11 |
12 | [Live Demo](http://srackham.github.io/riot-todo/)
13 |
14 |
15 | ## Differences between this application and the Flux Backbone Todos Example
16 | 1. Uses [Riot](https://muut.com/riotjs/) UI library instead of [React](http://facebook.github.io/react/).
17 | 2. Uses [RiotControl](https://github.com/jimsparkman/RiotControl) (slightly modified) instead of the [Flux](https://github.com/facebook/flux) dispatcher.
18 | 3. Writes to browser LocalStorage directly instead of using [Backbone.js](http://backbonejs.org/).
19 |
20 | Apart from that the application functionality and architecture is the same.
21 |
22 |
23 | ## Building and Running
24 | The app is developed and built in a node/npm environment. To install
25 | and run:
26 |
27 | 1. Make sure you have node and npm installed.
28 |
29 | 2. Clone the Github repo:
30 |
31 | git clone https://github.com/srackham/riot-todo.git
32 |
33 | 3. Install npm dependencies:
34 |
35 | cd riot-todo
36 | npm install
37 |
38 | 4. Build the app `dist/bundle.js` bundle:
39 |
40 | webpack
41 |
42 | 5. Start the app in a server:
43 |
44 | npm start
45 |
46 | 6. Open your Web browser at .
47 |
48 |
--------------------------------------------------------------------------------
/build/bundle.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId])
10 | /******/ return installedModules[moduleId].exports;
11 | /******/
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ exports: {},
15 | /******/ id: moduleId,
16 | /******/ loaded: false
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.loaded = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // __webpack_public_path__
37 | /******/ __webpack_require__.p = "";
38 | /******/
39 | /******/ // Load entry module and return exports
40 | /******/ return __webpack_require__(0);
41 | /******/ })
42 | /************************************************************************/
43 | /******/ ([
44 | /* 0 */
45 | /***/ function(module, exports, __webpack_require__) {
46 |
47 | /*
48 | Simple Todo app. Port of React/Flux Todo app https://github.com/srackham/flux-backbone-todo
49 | */
50 | "use strict";
51 |
52 | var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };
53 |
54 | var riot = _interopRequire(__webpack_require__(4));
55 |
56 | var TodoStore = _interopRequire(__webpack_require__(1));
57 |
58 | var dispatcher = _interopRequire(__webpack_require__(3));
59 |
60 | __webpack_require__(2);
61 |
62 | var todoStore = new TodoStore(dispatcher);
63 | dispatcher.addStore(todoStore);
64 | riot.mount("todo-app", { store: todoStore });
65 |
66 | /***/ },
67 | /* 1 */
68 | /***/ function(module, exports, __webpack_require__) {
69 |
70 | // TodoStore definition.
71 | // Flux stores house application logic and state that relate to a specific domain.
72 | // The store responds to relevant events emitted by the flux dispatcher.
73 | // The store emits change events to any listening views, so that they may react
74 | // and redraw themselves.
75 | "use strict";
76 |
77 | var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };
78 |
79 | module.exports = TodoStore;
80 | var Riot = _interopRequire(__webpack_require__(4));
81 |
82 | function TodoStore(dispatcher) {
83 | var _this = this;
84 | var LOCALSTORAGE_KEY = "riot-todo";
85 | Riot.observable(this); // Riot provides our event emitter.
86 | this.CHANGED_EVENT = "CHANGED_EVENT";
87 | var json = window.localStorage.getItem(LOCALSTORAGE_KEY);
88 | this.todos = json && JSON.parse(json) || [];
89 | this.dispatcher = dispatcher;
90 |
91 | var triggerChanged = function () {
92 | // Brute force update all.
93 | window.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(_this.todos));
94 | _this.trigger(_this.CHANGED_EVENT);
95 | };
96 |
97 | // Event handlers.
98 | this.on(dispatcher.ADD_TODO, function (todo) {
99 | _this.todos.push(todo);
100 | triggerChanged();
101 | });
102 |
103 | this.on(dispatcher.TOGGLE_TODO, function (todo) {
104 | todo.done = !todo.done;
105 | triggerChanged();
106 | });
107 |
108 | this.on(dispatcher.CLEAR_TODOS, function () {
109 | _this.todos = _this.todos.filter(function (todoItem) {
110 | return !todoItem.done;
111 | });
112 | triggerChanged();
113 | });
114 |
115 | this.on(dispatcher.INIT_TODOS, function () {
116 | triggerChanged();
117 | });
118 | }
119 |
120 | /***/ },
121 | /* 2 */
122 | /***/ function(module, exports, __webpack_require__) {
123 |
124 | "use strict";
125 |
126 | var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };
127 |
128 | var riot = _interopRequire(__webpack_require__(4));
129 |
130 | riot.tag("todo-app", "
Todos
\n \n \n \n Want a second fully synchronized list? Just declare another list component:\n no code required, no events to wire up!\n
\n ", function (opts) {
131 | var dispatcher = this.opts.store.dispatcher;
132 | this.on("mount", function () {
133 | return dispatcher.trigger(dispatcher.INIT_TODOS);
134 | });
135 | });
136 |
137 |
138 | riot.tag("todo-form", "\n ", function (opts) {
139 | var _this = this;
140 | var store = this.opts.store;
141 | var dispatcher = store.dispatcher;
142 |
143 | this.add = function (e) {
144 | if (_this.input.value) {
145 | dispatcher.trigger(dispatcher.ADD_TODO, { title: _this.input.value, done: false });
146 | _this.input.value = "";
147 | }
148 | };
149 |
150 | this.clear = function (e) {
151 | dispatcher.trigger(dispatcher.CLEAR_TODOS);
152 | };
153 | });
154 |
155 |
156 | riot.tag("todo-list", "", function (opts) {
157 | var _this = this;
158 | var store = this.opts.store;
159 | store.on(store.CHANGED_EVENT, function () {
160 | return _this.update();
161 | });
162 | });
163 |
164 |
165 | riot.tag("todo-item", "\n {opts.todo.title}\n ", function (opts) {
166 | var dispatcher = this.opts.store.dispatcher;
167 |
168 | this.toggle = function () {
169 | dispatcher.trigger(dispatcher.TOGGLE_TODO, opts.todo);
170 | };
171 | });
172 |
173 | /***/ },
174 | /* 3 */
175 | /***/ function(module, exports, __webpack_require__) {
176 |
177 | "use strict";
178 |
179 | // RiotControl dispatcher with Todo actions and formatted as node commonjs module.
180 | // https://github.com/jimsparkman/RiotControl
181 |
182 | module.exports = {
183 |
184 | // Dispatcher actions.
185 | ADD_TODO: "ADD_TODO",
186 | TOGGLE_TODO: "TOGGLE_TODO",
187 | CLEAR_TODOS: "CLEAR_TODOS",
188 | INIT_TODOS: "INIT_TODOS",
189 |
190 | _stores: [],
191 |
192 | addStore: function addStore(store) {
193 | this._stores.push(store);
194 | },
195 |
196 | trigger: function trigger() {
197 | var args = [].slice.call(arguments);
198 | console.log("dispatcher: trigger: " + args);
199 | this._stores.forEach(function (el) {
200 | el.trigger.apply(null, args);
201 | });
202 | },
203 |
204 | on: function on(ev, cb) {
205 | this._stores.forEach(function (el) {
206 | el.on(ev, cb);
207 | });
208 | },
209 |
210 | off: function off(ev, cb) {
211 | this._stores.forEach(function (el) {
212 | if (cb) el.off(ev, cb);else el.off(ev);
213 | });
214 | },
215 |
216 | one: function one(ev, cb) {
217 | this._stores.forEach(function (el) {
218 | el.one(ev, cb);
219 | });
220 | }
221 |
222 | };
223 |
224 | /***/ },
225 | /* 4 */
226 | /***/ function(module, exports, __webpack_require__) {
227 |
228 | /* Riot v2.0.9, @license MIT, (c) 2015 Muut Inc. + contributors */
229 |
230 | ;(function() {
231 |
232 | var riot = { version: 'v2.0.9', settings: {} }
233 |
234 | 'use strict'
235 |
236 | riot.observable = function(el) {
237 |
238 | el = el || {}
239 |
240 | var callbacks = {},
241 | _id = 0
242 |
243 | el.on = function(events, fn) {
244 | if (typeof fn == 'function') {
245 | fn._id = _id++
246 |
247 | events.replace(/\S+/g, function(name, pos) {
248 | (callbacks[name] = callbacks[name] || []).push(fn)
249 | fn.typed = pos > 0
250 | })
251 | }
252 | return el
253 | }
254 |
255 | el.off = function(events, fn) {
256 | if (events == '*') callbacks = {}
257 | else if (fn) {
258 | var arr = callbacks[events]
259 | for (var i = 0, cb; (cb = arr && arr[i]); ++i) {
260 | if (cb._id == fn._id) { arr.splice(i, 1); i-- }
261 | }
262 | } else {
263 | events.replace(/\S+/g, function(name) {
264 | callbacks[name] = []
265 | })
266 | }
267 | return el
268 | }
269 |
270 | // only single event supported
271 | el.one = function(name, fn) {
272 | if (fn) fn.one = 1
273 | return el.on(name, fn)
274 | }
275 |
276 | el.trigger = function(name) {
277 | var args = [].slice.call(arguments, 1),
278 | fns = callbacks[name] || []
279 |
280 | for (var i = 0, fn; (fn = fns[i]); ++i) {
281 | if (!fn.busy) {
282 | fn.busy = 1
283 | fn.apply(el, fn.typed ? [name].concat(args) : args)
284 | if (fn.one) { fns.splice(i, 1); i-- }
285 | else if (fns[i] !== fn) { i-- } // Makes self-removal possible during iteration
286 | fn.busy = 0
287 | }
288 | }
289 |
290 | return el
291 | }
292 |
293 | return el
294 |
295 | }
296 | ;(function(riot, evt) {
297 |
298 | // browsers only
299 | if (!this.top) return
300 |
301 | var loc = location,
302 | fns = riot.observable(),
303 | win = window,
304 | current
305 |
306 | function hash() {
307 | return loc.hash.slice(1)
308 | }
309 |
310 | function parser(path) {
311 | return path.split('/')
312 | }
313 |
314 | function emit(path) {
315 | if (path.type) path = hash()
316 |
317 | if (path != current) {
318 | fns.trigger.apply(null, ['H'].concat(parser(path)))
319 | current = path
320 | }
321 | }
322 |
323 | var r = riot.route = function(arg) {
324 | // string
325 | if (arg[0]) {
326 | loc.hash = arg
327 | emit(arg)
328 |
329 | // function
330 | } else {
331 | fns.on('H', arg)
332 | }
333 | }
334 |
335 | r.exec = function(fn) {
336 | fn.apply(null, parser(hash()))
337 | }
338 |
339 | r.parser = function(fn) {
340 | parser = fn
341 | }
342 |
343 | win.addEventListener ? win.addEventListener(evt, emit, false) : win.attachEvent('on' + evt, emit)
344 |
345 | })(riot, 'hashchange')
346 | /*
347 |
348 | //// How it works?
349 |
350 |
351 | Three ways:
352 |
353 | 1. Expressions: tmpl('{ value }', data).
354 | Returns the result of evaluated expression as a raw object.
355 |
356 | 2. Templates: tmpl('Hi { name } { surname }', data).
357 | Returns a string with evaluated expressions.
358 |
359 | 3. Filters: tmpl('{ show: !done, highlight: active }', data).
360 | Returns a space separated list of trueish keys (mainly
361 | used for setting html classes), e.g. "show highlight".
362 |
363 |
364 | // Template examples
365 |
366 | tmpl('{ title || "Untitled" }', data)
367 | tmpl('Results are { results ? "ready" : "loading" }', data)
368 | tmpl('Today is { new Date() }', data)
369 | tmpl('{ message.length > 140 && "Message is too long" }', data)
370 | tmpl('This item got { Math.round(rating) } stars', data)
371 | tmpl('{ title }
{ body }', data)
372 |
373 |
374 | // Falsy expressions in templates
375 |
376 | In templates (as opposed to single expressions) all falsy values
377 | except zero (undefined/null/false) will default to empty string:
378 |
379 | tmpl('{ undefined } - { false } - { null } - { 0 }', {})
380 | // will return: " - - - 0"
381 |
382 |
383 | // Customizable brackets
384 |
385 | riot.settings.brackets = '[ ]'
386 | riot.settings.brackets = '<% %>'
387 |
388 | */
389 |
390 | var tmpl = (function() {
391 |
392 | var cache = {},
393 | brackets,
394 | re_expr,
395 | re_vars = /("|').+?[^\\]\1|\.\w*|\w*:|\b(?:(?:new|typeof|in|instanceof) |(?:this|true|false|null|undefined)\b|function *\()|([a-z_]\w*)/gi
396 | // [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ]
397 | // find variable names:
398 | // 1. skip quoted strings: "a b", 'a b', 'a \'b\''
399 | // 2. skip object properties: .name
400 | // 3. skip object literals: name:
401 | // 4. skip javascript keywords
402 | // 5. match var name
403 |
404 | return function(str, data) {
405 |
406 | // make sure we use current brackets setting
407 | var b = riot.settings.brackets || '{ }'
408 | if (b != brackets) {
409 | brackets = b.split(' ')
410 | re_expr = re(/({[\s\S]*?})/)
411 | }
412 |
413 | // build a template (or get it from cache), render with data
414 | // (or just test if string has expression when called w/o data)
415 | return data
416 | ? str && (cache[str] = cache[str] || tmpl(str))(data)
417 | : re_expr.test(str)
418 |
419 | }
420 |
421 |
422 | // create a template instance
423 |
424 | function tmpl(s, p) {
425 |
426 | // default template string to {}
427 | p = (s || brackets.join(''))
428 |
429 | // temporarily convert \{ and \} to a non-character
430 | .replace(re(/\\{/), '\uFFF0')
431 | .replace(re(/\\}/), '\uFFF1')
432 |
433 | // split string to expression and non-expresion parts
434 | .split(re_expr)
435 |
436 | return new Function('d', 'return ' + (
437 |
438 | // is it a single expression or a template? i.e. {x} or {x}
439 | !p[0] && !p[2] && !p[3]
440 |
441 | // if expression, evaluate it
442 | ? expr(p[1])
443 |
444 | // if template, evaluate all expressions in it
445 | : '[' + p.map(function(s, i) {
446 |
447 | // is it an expression or a string (every second part is an expression)
448 | return i % 2
449 |
450 | // evaluate the expressions
451 | ? expr(s, 1)
452 |
453 | // process string parts of the template:
454 | : '"' + s
455 |
456 | // preserve new lines
457 | .replace(/\n/g, '\\n')
458 |
459 | // escape quotes
460 | .replace(/"/g, '\\"')
461 |
462 | + '"'
463 |
464 | }).join(',') + '].join("")'
465 | )
466 |
467 | // bring escaped { and } back
468 | .replace(/\uFFF0/g, brackets[0])
469 | .replace(/\uFFF1/g, brackets[1])
470 |
471 | )
472 |
473 | }
474 |
475 |
476 | // parse { ... } expression
477 |
478 | function expr(s, n) {
479 | s = s
480 |
481 | // convert new lines to spaces
482 | .replace(/\n/g, ' ')
483 |
484 | // trim whitespace, curly brackets, strip comments
485 | .replace(re(/^[{ ]+|[ }]+$|\/\*.+?\*\//g), '')
486 |
487 | // is it an object literal? i.e. { key : value }
488 | return /^\s*[\w- "']+ *:/.test(s)
489 |
490 | // if object literal, return trueish keys
491 | // e.g.: { show: isOpen(), done: item.done } -> "show done"
492 | ? '[' + s.replace(/\W*([\w- ]+)\W*:([^,]+)/g, function(_, k, v) {
493 |
494 | // safely execute vars to prevent undefined value errors
495 | return v.replace(/\w[^,|& ]*/g, function(v) { return wrap(v, n) }) + '?"' + k.trim() + '":"",'
496 |
497 | }) + '].join(" ")'
498 |
499 | // if js expression, evaluate as javascript
500 | : wrap(s, n)
501 |
502 | }
503 |
504 |
505 | // execute js w/o breaking on errors or undefined vars
506 |
507 | function wrap(s, nonull) {
508 | return '(function(v){try{v='
509 |
510 | // prefix vars (name => data.name)
511 | + (s.replace(re_vars, function(s, _, v) { return v ? '(d.'+v+'===undefined?window.'+v+':d.'+v+')' : s })
512 |
513 | // break the expression if its empty (resulting in undefined value)
514 | || 'x')
515 |
516 | + '}finally{return '
517 |
518 | // default to empty string for falsy values except zero
519 | + (nonull ? '!v&&v!==0?"":v' : 'v')
520 |
521 | + '}}).call(d)'
522 | }
523 |
524 |
525 | // change regexp to use custom brackets
526 |
527 | function re(r) {
528 | return RegExp(r.source
529 | .split('{').join('\\'+brackets[0])
530 | .split('}').join('\\'+brackets[1]),
531 | r.global ? 'g' : '')
532 | }
533 |
534 | })()
535 | // { key, i in items} -> { key, i, items }
536 | function loopKeys(expr) {
537 | var ret = { val: expr },
538 | els = expr.split(/\s+in\s+/)
539 |
540 | if (els[1]) {
541 | ret.val = expr_begin + els[1]
542 | els = els[0].slice(expr_begin.length).trim().split(/,\s*/)
543 | ret.key = els[0]
544 | ret.pos = els[1]
545 | }
546 |
547 | return ret
548 | }
549 |
550 | function mkitem(expr, key, val) {
551 | var item = {}
552 | item[expr.key] = key
553 | if (expr.pos) item[expr.pos] = val
554 | return item
555 | }
556 |
557 | function _each(dom, parent, expr) {
558 |
559 | remAttr(dom, 'each')
560 |
561 | var template = dom.outerHTML,
562 | prev = dom.previousSibling,
563 | root = dom.parentNode,
564 | rendered = [],
565 | tags = [],
566 | checksum
567 |
568 | expr = loopKeys(expr)
569 |
570 | // clean template code after update (and let walk finish it's parse)
571 | parent.one('update', function() {
572 | root.removeChild(dom)
573 |
574 | }).one('mount', function() {
575 | if (root.stub) root = parent.root
576 |
577 | }).on('update', function() {
578 |
579 | var items = tmpl(expr.val, parent)
580 | if (!items) return
581 |
582 | // object loop. any changes cause full redraw
583 | if (!Array.isArray(items)) {
584 | var testsum = JSON.stringify(items)
585 | if (testsum == checksum) return
586 | checksum = testsum
587 |
588 | // clear old items
589 | tags.map(function(tag) { tag.unmount() })
590 | tags = rendered = []
591 |
592 | items = Object.keys(items).map(function(key) {
593 | return mkitem(expr, key, items[key])
594 | })
595 |
596 | }
597 |
598 | // unmount redundant
599 | arrDiff(rendered, items).map(function(item) {
600 | var pos = rendered.indexOf(item),
601 | tag = tags[pos]
602 |
603 | if (tag) {
604 | tag.unmount()
605 | rendered.splice(pos, 1)
606 | tags.splice(pos, 1)
607 | }
608 |
609 | })
610 |
611 | // mount new
612 | var nodes = root.childNodes,
613 | prev_index = [].indexOf.call(nodes, prev)
614 |
615 | arrDiff(items, rendered).map(function(item, i) {
616 |
617 | var pos = items.indexOf(item)
618 |
619 | if (!checksum && expr.key) item = mkitem(expr, item, pos)
620 |
621 | var tag = new Tag({ tmpl: template }, {
622 | before: nodes[prev_index + 1 + pos],
623 | parent: parent,
624 | root: root,
625 | loop: true,
626 | item: item
627 | })
628 |
629 | tags.splice(pos, 0, tag)
630 |
631 | })
632 |
633 | rendered = items.slice()
634 |
635 | })
636 |
637 | }
638 | function parseNamedElements(root, tag, expressions) {
639 | walk(root, function(dom) {
640 | if (dom.nodeType != 1) return
641 |
642 | each(dom.attributes, function(attr) {
643 | if (/^(name|id)$/.test(attr.name)) tag[attr.value] = dom
644 | })
645 | })
646 | }
647 |
648 | function parseLayout(root, tag, expressions) {
649 |
650 | function addExpr(dom, val, extra) {
651 | if (val.indexOf(expr_begin) >= 0) {
652 | var expr = { dom: dom, expr: val }
653 | expressions.push(extend(expr, extra))
654 | }
655 | }
656 |
657 | walk(root, function(dom) {
658 |
659 | var type = dom.nodeType
660 |
661 | // text node
662 | if (type == 3 && dom.parentNode.tagName != 'STYLE') addExpr(dom, dom.nodeValue)
663 | if (type != 1) return
664 |
665 | /* element */
666 |
667 | // loop
668 | var attr = dom.getAttribute('each')
669 | if (attr) { _each(dom, tag, attr); return false }
670 |
671 | // attributes
672 | each(dom.attributes, function(attr) {
673 | var name = attr.name,
674 | value = attr.value
675 |
676 | // expressions
677 | var bool = name.split('__')[1]
678 | addExpr(dom, value, { attr: bool || name, bool: bool })
679 |
680 | if (bool) {
681 | remAttr(dom, name)
682 | return false
683 | }
684 |
685 | })
686 |
687 | // child tag
688 | var impl = tag_impl[dom.tagName.toLowerCase()]
689 | if (impl) impl = new Tag(impl, { root: dom, parent: tag })
690 |
691 | })
692 |
693 | }
694 | function Tag(impl, conf) {
695 |
696 | var self = riot.observable(this),
697 | expressions = [],
698 | attributes = {},
699 | parent = conf.parent,
700 | is_loop = conf.loop,
701 | root = conf.root,
702 | opts = conf.opts,
703 | item = conf.item
704 |
705 | // cannot initialize twice on the same root element
706 | if (!is_loop && root.riot) return
707 | root.riot = 1
708 |
709 | opts = opts || {}
710 |
711 | extend(this, { parent: parent, root: root, opts: opts })
712 | extend(this, item)
713 |
714 |
715 | // attributes
716 | each(root.attributes, function(attr) {
717 | var name = attr.name,
718 | val = attr.value
719 |
720 | attributes[name] = val
721 |
722 | // remove dynamic attributes from node
723 | if (val.indexOf(expr_begin) >= 0) {
724 | remAttr(root, name)
725 | return false
726 | }
727 | })
728 |
729 | // options
730 | function updateOpts() {
731 | Object.keys(attributes).map(function(name) {
732 | opts[name] = tmpl(attributes[name], parent || self)
733 | })
734 | }
735 |
736 | updateOpts()
737 |
738 | // child
739 | var dom = mkdom(impl.tmpl),
740 | loop_dom
741 |
742 | // named elements
743 | parseNamedElements(dom, this)
744 |
745 | this.update = function(data, init) {
746 | extend(self, data)
747 | extend(self, item)
748 | self.trigger('update')
749 | updateOpts()
750 | update(expressions, self, item)
751 | self.trigger('updated')
752 | }
753 |
754 | this.unmount = function() {
755 | var el = is_loop ? loop_dom : root,
756 | p = el.parentNode
757 |
758 | if (p) {
759 | p.removeChild(el)
760 | self.trigger('unmount')
761 | parent && parent.off('update', self.update)
762 | }
763 | }
764 |
765 | function mount() {
766 |
767 | if (is_loop) {
768 | loop_dom = dom.firstChild
769 | root.insertBefore(loop_dom, conf.before || null) // null needed for IE8
770 |
771 | } else {
772 | while (dom.firstChild) root.appendChild(dom.firstChild)
773 | }
774 |
775 | if (root.stub) self.root = root = parent.root
776 |
777 | self.trigger('mount')
778 |
779 | // one way data flow: propagate updates and unmounts downwards from parent to children
780 | parent && parent.on('update', self.update).one('unmount', self.unmount)
781 |
782 | }
783 |
784 | // initialize
785 | if (impl.fn) impl.fn.call(this, opts)
786 |
787 | // layout
788 | parseLayout(dom, this, expressions)
789 |
790 | this.update()
791 | mount()
792 |
793 | }
794 |
795 |
796 | function setEventHandler(name, handler, dom, tag, item) {
797 |
798 | dom[name] = function(e) {
799 |
800 | // cross browser event fix
801 | e = e || window.event
802 | e.which = e.which || e.charCode || e.keyCode
803 | e.target = e.target || e.srcElement
804 | e.currentTarget = dom
805 | e.item = item
806 |
807 | // prevent default behaviour (by default)
808 | if (handler.call(tag, e) !== true) {
809 | e.preventDefault && e.preventDefault()
810 | e.returnValue = false
811 | }
812 |
813 | tag.update()
814 | }
815 |
816 | }
817 |
818 | function insertTo(root, node, before) {
819 | if (root) {
820 | root.insertBefore(before, node)
821 | root.removeChild(node)
822 | }
823 | }
824 |
825 | // item = currently looped item
826 | function update(expressions, tag, item) {
827 |
828 | each(expressions, function(expr) {
829 | var dom = expr.dom,
830 | attr_name = expr.attr,
831 | value = tmpl(expr.expr, tag)
832 |
833 | if (value == null) value = ''
834 |
835 | // no change
836 | if (expr.value === value) return
837 | expr.value = value
838 |
839 | // text node
840 | if (!attr_name) return dom.nodeValue = value
841 |
842 | // remove attribute
843 | if (!value && expr.bool || /obj|func/.test(typeof value)) remAttr(dom, attr_name)
844 |
845 | // event handler
846 | if (typeof value == 'function') {
847 | setEventHandler(attr_name, value, dom, tag, item)
848 |
849 | // if- conditional
850 | } else if (attr_name == 'if') {
851 | remAttr(dom, attr_name)
852 |
853 | var stub = expr.stub
854 |
855 | // add to DOM
856 | if (value) {
857 | stub && insertTo(stub.parentNode, stub, dom)
858 |
859 | // remove from DOM
860 | } else {
861 | stub = expr.stub = stub || document.createTextNode('')
862 | insertTo(dom.parentNode, dom, stub)
863 | }
864 |
865 | // show / hide
866 | } else if (/^(show|hide)$/.test(attr_name)) {
867 | remAttr(dom, attr_name)
868 | if (attr_name == 'hide') value = !value
869 | dom.style.display = value ? '' : 'none'
870 |
871 | // normal attribute
872 | } else if (attr_name == 'value') {
873 | dom.value = value
874 |
875 | } else {
876 | if (expr.bool) {
877 | dom[attr_name] = value
878 | if (!value) return
879 | value = attr_name
880 | }
881 |
882 | dom.setAttribute(attr_name, value)
883 | }
884 |
885 | })
886 |
887 | }
888 | function each(nodes, fn) {
889 | for (var i = 0; i < (nodes || []).length; i++) {
890 | if (fn(nodes[i], i) === false) i--
891 | }
892 | }
893 |
894 | function remAttr(dom, name) {
895 | dom.removeAttribute(name)
896 | }
897 |
898 | function extend(obj, from) {
899 | from && Object.keys(from).map(function(key) {
900 | obj[key] = from[key]
901 | })
902 | return obj
903 | }
904 |
905 | function mkdom(template) {
906 | var tag_name = template.trim().slice(1, 3).toLowerCase(),
907 | root_tag = /td|th/.test(tag_name) ? 'tr' : tag_name == 'tr' ? 'tbody' : 'div',
908 | el = document.createElement(root_tag)
909 |
910 | el.stub = true
911 | el.innerHTML = template
912 | return el
913 | }
914 |
915 | function walk(dom, fn) {
916 | dom = fn(dom) === false ? dom.nextSibling : dom.firstChild
917 |
918 | while (dom) {
919 | walk(dom, fn)
920 | dom = dom.nextSibling
921 | }
922 | }
923 |
924 | function arrDiff(arr1, arr2) {
925 | return arr1.filter(function(el) {
926 | return arr2.indexOf(el) < 0
927 | })
928 | }
929 |
930 | /*
931 | Virtual dom is an array of custom tags on the document.
932 | Updates and unmounts propagate downwards from parent to children.
933 | */
934 |
935 | var virtual_dom = [],
936 | tag_impl = {},
937 | expr_begin
938 |
939 | riot.tag = function(name, html, fn) {
940 | expr_begin = expr_begin || (riot.settings.brackets || '{ }').split(' ')[0]
941 | tag_impl[name] = { name: name, tmpl: html, fn: fn }
942 | }
943 |
944 | var mountTo = riot.mountTo = function(root, tagName, opts) {
945 | var impl = tag_impl[tagName], tag
946 |
947 | if (impl && root) {
948 | root.riot = 0 // mountTo can override previous instance
949 | tag = new Tag(impl, { root: root, opts: opts })
950 | }
951 |
952 | if (tag) {
953 | virtual_dom.push(tag)
954 | return tag.on('unmount', function() {
955 | virtual_dom.splice(virtual_dom.indexOf(tag), 1)
956 | })
957 | }
958 | }
959 |
960 | riot.mount = function(selector, opts) {
961 | if (selector == '*') selector = Object.keys(tag_impl).join(', ')
962 |
963 | var tags = []
964 |
965 | each(document.querySelectorAll(selector), function(root) {
966 |
967 | var tagName = root.tagName.toLowerCase(),
968 | tag = mountTo(root, tagName, opts)
969 |
970 | if (tag) tags.push(tag)
971 | })
972 |
973 | return tags
974 | }
975 |
976 | // update everything
977 | riot.update = function() {
978 | virtual_dom.map(function(tag) {
979 | tag.update()
980 | })
981 | return virtual_dom
982 | }
983 |
984 |
985 | // support CommonJS
986 | if (true)
987 | module.exports = riot
988 |
989 | // support AMD
990 | else if (typeof define === 'function' && define.amd)
991 | define(function() { return riot })
992 |
993 | // support browser
994 | else
995 | this.riot = riot
996 |
997 | })();
998 |
999 |
1000 | /***/ }
1001 | /******/ ])
1002 | //# sourceMappingURL=bundle.js.map
--------------------------------------------------------------------------------
/build/index.css:
--------------------------------------------------------------------------------
1 | todo-item {
2 | cursor: pointer;
3 | }
4 | todo-item .done {
5 | text-decoration: line-through;
6 | }
7 |
--------------------------------------------------------------------------------
/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Riot Todo App
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/deploy-gh-pages.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Rebuild, copy, commit then upload gh-pages branch.
3 |
4 | # Exit immediately on error.
5 | set -e
6 |
7 | echo -e "\033[0;32mDeploying Github Pages...\033[0m"
8 |
9 | # Check that there are no uncommited files in master.
10 | status=$(git status --short)
11 | if [ $(echo "$status" | wc -w) -gt 0 ]; then
12 | echo "FAILED: There are uncommited changes."
13 | echo "$status"
14 | exit 1
15 | fi
16 |
17 | # Rebild application.
18 | webpack
19 |
20 | # Copy built files to gh-pages.
21 | cp -p ./build/* ./gh-pages/
22 |
23 | cd ./gh-pages/
24 |
25 | # Add changes to git.
26 | git add .
27 |
28 | # Commit changes.
29 | msg="Rebuild Github Pages."
30 | if [ $# -eq 1 ]
31 | then msg="$1"
32 | fi
33 | git commit -m "$msg"
34 |
35 | # Push to Github Pages.
36 | git push origin gh-pages
37 |
38 | cd ..
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "riot-todo",
3 | "author": "Stuart Rackham",
4 | "description": "Didactic Flux-like ES6 Todo app written using Riot.",
5 | "license": "MIT",
6 | "version": "0.1.5",
7 | "keywords": [
8 | "flux",
9 | "riot",
10 | "riotjs",
11 | "es6",
12 | "todo",
13 | "todomvc"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/srackham/riot-todo"
18 | },
19 | "dependencies": {},
20 | "devDependencies": {
21 | "babel": "^4.7.12",
22 | "babel-loader": "^4.1.0",
23 | "riot": "^2.0.13",
24 | "webpack": "^1.7.3",
25 | "webpack-dev-server": "^1.7.0"
26 | },
27 | "scripts": {
28 | "build": "webpack --progress --colors",
29 | "deploy-gh-pages": "./deploy-gh-pages.sh",
30 | "push": "git push --tags origin master",
31 | "start": "webpack-dev-server --progress --colors --content-base build"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/dispatcher.js:
--------------------------------------------------------------------------------
1 | // RiotControl dispatcher with Todo actions and formatted as node commonjs module.
2 | // https://github.com/jimsparkman/RiotControl
3 |
4 | module.exports = {
5 |
6 | // Dispatcher actions.
7 | ADD_TODO: 'ADD_TODO',
8 | TOGGLE_TODO: 'TOGGLE_TODO',
9 | CLEAR_TODOS: 'CLEAR_TODOS',
10 | INIT_TODOS: 'INIT_TODOS',
11 |
12 | _stores: [],
13 |
14 | addStore(store) {
15 | this._stores.push(store)
16 | },
17 |
18 | trigger() {
19 | let args = [].slice.call(arguments);
20 | console.log('dispatcher: trigger: ' + args);
21 | this._stores.forEach(function(el) {
22 | el.trigger.apply(null, args)
23 | })
24 | },
25 |
26 | on(ev, cb) {
27 | this._stores.forEach(function(el) {
28 | el.on(ev, cb)
29 | })
30 | },
31 |
32 | off(ev, cb) {
33 | this._stores.forEach(function(el) {
34 | if (cb)
35 | el.off(ev, cb);
36 | else
37 | el.off(ev)
38 | })
39 | },
40 |
41 | one(ev, cb) {
42 | this._stores.forEach(function(el) {
43 | el.one(ev, cb)
44 | })
45 | }
46 |
47 | };
48 |
49 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | Simple Todo app. Port of React/Flux Todo app https://github.com/srackham/flux-backbone-todo
3 | */
4 | 'use strict';
5 |
6 | import riot from 'riot';
7 | import TodoStore from './todo-store.js';
8 | import dispatcher from './dispatcher.js';
9 | import './tags.js'
10 |
11 | let todoStore = new TodoStore(dispatcher);
12 | dispatcher.addStore(todoStore);
13 | riot.mount('todo-app', {store: todoStore});
14 |
--------------------------------------------------------------------------------
/src/tags.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot';
2 |
3 | riot.tag('todo-app',
4 |
5 | `Todos
6 |
7 |
8 |
9 | Want a second fully synchronized list? Just declare another list component:
10 | no code required, no events to wire up!
11 |
12 | `,
13 |
14 | function(opts) {
15 | let dispatcher = this.opts.store.dispatcher;
16 | this.on('mount', () => dispatcher.trigger(dispatcher.INIT_TODOS));
17 | }
18 |
19 | );
20 |
21 |
22 | riot.tag('todo-form',
23 |
24 | `
28 | `,
29 |
30 | function(opts) {
31 | let store = this.opts.store;
32 | let dispatcher = store.dispatcher;
33 |
34 | this.add = (e) => {
35 | if (this.input.value) {
36 | dispatcher.trigger(dispatcher.ADD_TODO, {title: this.input.value, done: false});
37 | this.input.value = ''
38 | }
39 | };
40 |
41 | this.clear = (e) => {
42 | dispatcher.trigger(dispatcher.CLEAR_TODOS);
43 | };
44 | }
45 |
46 | );
47 |
48 |
49 | riot.tag('todo-list',
50 |
51 | ``,
56 |
57 | function(opts) {
58 | let store = this.opts.store;
59 | store.on(store.CHANGED_EVENT, () => this.update());
60 | }
61 |
62 | );
63 |
64 |
65 | riot.tag('todo-item',
66 |
67 | `
68 | {opts.todo.title}
69 | `,
70 |
71 | function(opts) {
72 | let dispatcher = this.opts.store.dispatcher;
73 |
74 | this.toggle = () => {
75 | dispatcher.trigger(dispatcher.TOGGLE_TODO, opts.todo);
76 | }
77 | }
78 |
79 | );
80 |
--------------------------------------------------------------------------------
/src/todo-store.js:
--------------------------------------------------------------------------------
1 | // TodoStore definition.
2 | // Flux stores house application logic and state that relate to a specific domain.
3 | // The store responds to relevant events emitted by the flux dispatcher.
4 | // The store emits change events to any listening views, so that they may react
5 | // and redraw themselves.
6 | 'use strict';
7 |
8 | import Riot from 'riot';
9 |
10 | export default function TodoStore(dispatcher) {
11 | const LOCALSTORAGE_KEY = 'riot-todo';
12 | Riot.observable(this); // Riot provides our event emitter.
13 | this.CHANGED_EVENT = 'CHANGED_EVENT';
14 | let json = window.localStorage.getItem(LOCALSTORAGE_KEY);
15 | this.todos = (json && JSON.parse(json)) || [];
16 | this.dispatcher = dispatcher;
17 |
18 | let triggerChanged = () => {
19 | // Brute force update all.
20 | window.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(this.todos));
21 | this.trigger(this.CHANGED_EVENT);
22 | };
23 |
24 | // Event handlers.
25 | this.on(dispatcher.ADD_TODO, (todo) => {
26 | this.todos.push(todo);
27 | triggerChanged();
28 | });
29 |
30 | this.on(dispatcher.TOGGLE_TODO, (todo) => {
31 | todo.done = !todo.done;
32 | triggerChanged();
33 | });
34 |
35 | this.on(dispatcher.CLEAR_TODOS, () => {
36 | this.todos = this.todos.filter(todoItem => !todoItem.done);
37 | triggerChanged();
38 | });
39 |
40 | this.on(dispatcher.INIT_TODOS, () => {
41 | triggerChanged();
42 | });
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | cache: true,
3 | entry: './src/index.js',
4 | output: {
5 | path: __dirname + '/build/',
6 | filename: 'bundle.js'
7 | },
8 | devtool: 'source-map',
9 | module: {
10 | loaders: [
11 | {
12 | test: /\.js$/,
13 | include: /src/,
14 | loader: 'babel-loader',
15 | query: {modules: 'common'}
16 | },
17 | {
18 | test: /\.less$/,
19 | loader: 'style!css!less'
20 | },
21 | {
22 | test: /\.json$/,
23 | loader: 'json'
24 | }
25 | ]
26 | }
27 | };
28 |
--------------------------------------------------------------------------------