├── .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 \n \n
\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 | `
25 | 26 | 27 |
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 | --------------------------------------------------------------------------------